mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-03-12 04:01:22 +00:00
Compare commits
1 Commits
dev
...
feature-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8991b4ca |
18
.codecov.yml
18
.codecov.yml
@@ -14,6 +14,10 @@ component_management:
|
|||||||
# https://docs.codecov.com/docs/carryforward-flags
|
# https://docs.codecov.com/docs/carryforward-flags
|
||||||
flags:
|
flags:
|
||||||
# Backend Python versions
|
# Backend Python versions
|
||||||
|
backend-python-3.10:
|
||||||
|
paths:
|
||||||
|
- src/**
|
||||||
|
carryforward: true
|
||||||
backend-python-3.11:
|
backend-python-3.11:
|
||||||
paths:
|
paths:
|
||||||
- src/**
|
- src/**
|
||||||
@@ -22,14 +26,6 @@ flags:
|
|||||||
paths:
|
paths:
|
||||||
- src/**
|
- src/**
|
||||||
carryforward: true
|
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 (shards merge into single flag)
|
||||||
frontend-node-24.x:
|
frontend-node-24.x:
|
||||||
paths:
|
paths:
|
||||||
@@ -45,10 +41,9 @@ coverage:
|
|||||||
project:
|
project:
|
||||||
backend:
|
backend:
|
||||||
flags:
|
flags:
|
||||||
|
- backend-python-3.10
|
||||||
- backend-python-3.11
|
- backend-python-3.11
|
||||||
- backend-python-3.12
|
- backend-python-3.12
|
||||||
- backend-python-3.13
|
|
||||||
- backend-python-3.14
|
|
||||||
paths:
|
paths:
|
||||||
- src/**
|
- src/**
|
||||||
# https://docs.codecov.com/docs/commit-status#threshold
|
# https://docs.codecov.com/docs/commit-status#threshold
|
||||||
@@ -64,10 +59,9 @@ coverage:
|
|||||||
patch:
|
patch:
|
||||||
backend:
|
backend:
|
||||||
flags:
|
flags:
|
||||||
|
- backend-python-3.10
|
||||||
- backend-python-3.11
|
- backend-python-3.11
|
||||||
- backend-python-3.12
|
- backend-python-3.12
|
||||||
- backend-python-3.13
|
|
||||||
- backend-python-3.14
|
|
||||||
paths:
|
paths:
|
||||||
- src/**
|
- src/**
|
||||||
target: 100%
|
target: 100%
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ ARG RUNTIME_PACKAGES="\
|
|||||||
libmagic1 \
|
libmagic1 \
|
||||||
media-types \
|
media-types \
|
||||||
zlib1g \
|
zlib1g \
|
||||||
|
# Barcode splitter
|
||||||
|
libzbar0 \
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
htop \
|
htop \
|
||||||
sudo"
|
sudo"
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ Additional tasks are available for common maintenance operations:
|
|||||||
|
|
||||||
## Committing from the Host Machine
|
## Committing from the Host Machine
|
||||||
|
|
||||||
The DevContainer automatically installs Git pre-commit hooks during setup. However, these hooks are configured for use inside the container.
|
The DevContainer automatically installs pre-commit hooks during setup. However, these hooks are configured for use inside the container.
|
||||||
|
|
||||||
If you want to commit changes from your host machine (outside the DevContainer), you need to set up prek on your host. This installs it as a standalone tool.
|
If you want to commit changes from your host machine (outside the DevContainer), you need to set up pre-commit on your host. This installs it as a standalone tool.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv tool install prek && prek install
|
uv tool install pre-commit && pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
After this, you can commit either from inside the DevContainer or from your host machine.
|
After this, you can commit either from inside the DevContainer or from your host machine.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache"
|
"UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache"
|
||||||
},
|
},
|
||||||
"postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run prek install'",
|
"postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
@@ -116,9 +116,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Maintenance: Build Documentation",
|
"label": "Maintenance: Build Documentation",
|
||||||
"description": "Build the documentation with Zensical",
|
"description": "Build the documentation with MkDocs",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "uv run zensical build && uv run zensical serve",
|
"command": "uv run mkdocs build --config-file mkdocs.yml && uv run mkdocs serve",
|
||||||
"group": "none",
|
"group": "none",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"echo": true,
|
"echo": true,
|
||||||
|
|||||||
@@ -28,4 +28,3 @@
|
|||||||
./resources
|
./resources
|
||||||
# Other stuff
|
# Other stuff
|
||||||
**/*.drawio.png
|
**/*.drawio.png
|
||||||
.mypy_baseline
|
|
||||||
|
|||||||
@@ -39,6 +39,3 @@ max_line_length = off
|
|||||||
|
|
||||||
[Dockerfile*]
|
[Dockerfile*]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
[*.toml]
|
|
||||||
indent_style = space
|
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -37,6 +37,6 @@ NOTE: PRs that do not address the following will not be merged, please do not sk
|
|||||||
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
||||||
- [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
- [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||||
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
||||||
- [ ] I have run all Git `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
||||||
- [ ] I have made corresponding changes to the documentation as needed.
|
- [ ] I have made corresponding changes to the documentation as needed.
|
||||||
- [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR.
|
- [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR.
|
||||||
|
|||||||
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -46,8 +46,8 @@ updates:
|
|||||||
patterns:
|
patterns:
|
||||||
- "*pytest*"
|
- "*pytest*"
|
||||||
- "ruff"
|
- "ruff"
|
||||||
- "zensical"
|
- "mkdocs-material"
|
||||||
- "prek*"
|
- "pre-commit*"
|
||||||
# Django & DRF Ecosystem
|
# Django & DRF Ecosystem
|
||||||
django-ecosystem:
|
django-ecosystem:
|
||||||
patterns:
|
patterns:
|
||||||
@@ -69,6 +69,7 @@ updates:
|
|||||||
patterns:
|
patterns:
|
||||||
- "ocrmypdf"
|
- "ocrmypdf"
|
||||||
- "pdf2image"
|
- "pdf2image"
|
||||||
|
- "pyzbar"
|
||||||
- "zxing-cpp"
|
- "zxing-cpp"
|
||||||
- "tika-client"
|
- "tika-client"
|
||||||
- "gotenberg-client"
|
- "gotenberg-client"
|
||||||
|
|||||||
153
.github/workflows/ci-backend.yml
vendored
153
.github/workflows/ci-backend.yml
vendored
@@ -3,86 +3,50 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'translations**'
|
- 'translations**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
|
- 'docker/compose/docker-compose.ci-test.yml'
|
||||||
|
- '.github/workflows/ci-backend.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'translations**'
|
- 'translations**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
|
- 'docker/compose/docker-compose.ci-test.yml'
|
||||||
|
- '.github/workflows/ci-backend.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: backend-${{ github.event.pull_request.number || github.ref }}
|
group: backend-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.10.x"
|
DEFAULT_UV_VERSION: "0.9.x"
|
||||||
NLTK_DATA: "/usr/share/nltk_data"
|
NLTK_DATA: "/usr/share/nltk_data"
|
||||||
jobs:
|
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:
|
test:
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.backend_changed == 'true'
|
|
||||||
name: "Python ${{ matrix.python-version }}"
|
name: "Python ${{ matrix.python-version }}"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.11', '3.12', '3.13', '3.14']
|
python-version: ['3.10', '3.11', '3.12']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Start containers
|
- name: Start containers
|
||||||
run: |
|
run: |
|
||||||
docker compose --file docker/compose/docker-compose.ci-test.yml pull --quiet
|
docker compose --file docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||||
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6.2.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7.3.1
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -91,7 +55,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends \
|
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
|
- name: Configure ImageMagick
|
||||||
run: |
|
run: |
|
||||||
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
|
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
|
||||||
@@ -119,13 +83,13 @@ jobs:
|
|||||||
pytest
|
pytest
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
if: always()
|
if: always()
|
||||||
uses: codecov/codecov-action@v5.5.2
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: backend-python-${{ matrix.python-version }}
|
flags: backend-python-${{ matrix.python-version }}
|
||||||
files: junit.xml
|
files: junit.xml
|
||||||
report_type: test_results
|
report_type: test_results
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5.5.2
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: backend-python-${{ matrix.python-version }}
|
flags: backend-python-${{ matrix.python-version }}
|
||||||
files: coverage.xml
|
files: coverage.xml
|
||||||
@@ -135,80 +99,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker compose --file docker/compose/docker-compose.ci-test.yml logs
|
docker compose --file docker/compose/docker-compose.ci-test.yml logs
|
||||||
docker compose --file docker/compose/docker-compose.ci-test.yml down
|
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
|
|
||||||
- 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
|
|
||||||
with:
|
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
|
||||||
enable-cache: true
|
|
||||||
python-version: ${{ steps.setup-python.outputs.python-version }}
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: |
|
|
||||||
uv sync \
|
|
||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
|
||||||
--group testing \
|
|
||||||
--group typing \
|
|
||||||
--frozen
|
|
||||||
- name: List installed Python dependencies
|
|
||||||
run: |
|
|
||||||
uv pip list
|
|
||||||
- name: Check typing (pyrefly)
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
uv run pyrefly \
|
|
||||||
check \
|
|
||||||
src/
|
|
||||||
- name: Cache Mypy
|
|
||||||
uses: actions/cache@v5.0.3
|
|
||||||
with:
|
|
||||||
path: .mypy_cache
|
|
||||||
# Keyed by OS, Python version, and dependency hashes
|
|
||||||
key: ${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-${{ hashFiles('pyproject.toml', 'uv.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ 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."
|
|
||||||
|
|||||||
28
.github/workflows/ci-docker.yml
vendored
28
.github/workflows/ci-docker.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
ref-name: ${{ steps.ref.outputs.name }}
|
ref-name: ${{ steps.ref.outputs.name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6.0.1
|
||||||
- name: Determine ref name
|
- name: Determine ref name
|
||||||
id: ref
|
id: ref
|
||||||
run: |
|
run: |
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.12.0
|
uses: docker/setup-buildx-action@v3.12.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.7.0
|
uses: docker/login-action@v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6.19.2
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -149,16 +149,15 @@ jobs:
|
|||||||
mkdir -p /tmp/digests
|
mkdir -p /tmp/digests
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
echo "digest=${digest}"
|
echo "digest=${digest}"
|
||||||
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
if: steps.check-push.outputs.should-push == 'true'
|
if: steps.check-push.outputs.should-push == 'true'
|
||||||
uses: actions/upload-artifact@v7.0.0
|
uses: actions/upload-artifact@v6.0.0
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.arch }}
|
name: digests-${{ matrix.arch }}
|
||||||
path: /tmp/digests/digest-${{ matrix.arch }}.txt
|
path: /tmp/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
archive: false
|
|
||||||
merge-and-push:
|
merge-and-push:
|
||||||
name: Merge and Push Manifest
|
name: Merge and Push Manifest
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -169,10 +168,10 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v8.0.0
|
uses: actions/download-artifact@v7.0.0
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digest-*.txt
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: List digests
|
- name: List digests
|
||||||
run: |
|
run: |
|
||||||
@@ -181,20 +180,20 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.12.0
|
uses: docker/setup-buildx-action@v3.12.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.7.0
|
uses: docker/login-action@v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@v3.7.0
|
uses: docker/login-action@v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@v3.7.0
|
uses: docker/login-action@v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
@@ -218,9 +217,8 @@ jobs:
|
|||||||
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}")
|
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}")
|
||||||
|
|
||||||
digests=""
|
digests=""
|
||||||
for digest_file in digest-*.txt; do
|
for digest in *; do
|
||||||
digest=$(cat "${digest_file}")
|
digests+="${{ env.REGISTRY }}/${REPOSITORY}@sha256:${digest} "
|
||||||
digests+="${{ env.REGISTRY }}/${REPOSITORY}@${digest} "
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Creating manifest with tags: ${tags}"
|
echo "Creating manifest with tags: ${tags}"
|
||||||
|
|||||||
140
.github/workflows/ci-docs.yml
vendored
140
.github/workflows/ci-docs.yml
vendored
@@ -1,83 +1,39 @@
|
|||||||
name: Documentation
|
name: Documentation
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches:
|
||||||
- 'translations**'
|
- main
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
- 'mkdocs.yml'
|
||||||
|
- '.github/workflows/ci-docs.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
- 'mkdocs.yml'
|
||||||
|
- '.github/workflows/ci-docs.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: docs-${{ github.event.pull_request.number || github.ref }}
|
group: docs-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.10.x"
|
DEFAULT_UV_VERSION: "0.9.x"
|
||||||
DEFAULT_PYTHON_VERSION: "3.12"
|
DEFAULT_PYTHON_VERSION: "3.11"
|
||||||
jobs:
|
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:
|
build:
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.docs_changed == 'true'
|
|
||||||
name: Build Documentation
|
name: Build Documentation
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/configure-pages@v5.0.0
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6.2.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7.3.1
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -91,42 +47,42 @@ jobs:
|
|||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
--frozen \
|
--frozen \
|
||||||
zensical build --clean
|
mkdocs build --config-file ./mkdocs.yml
|
||||||
- name: Upload GitHub Pages artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v4.0.0
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: site
|
name: documentation
|
||||||
name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
|
path: site/
|
||||||
|
retention-days: 7
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy Documentation
|
name: Deploy Documentation
|
||||||
needs: [changes, build]
|
needs: build
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.docs_changed == 'true'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy GitHub Pages
|
- name: Checkout
|
||||||
uses: actions/deploy-pages@v4.0.5
|
uses: actions/checkout@v6
|
||||||
id: deployment
|
- name: Set up Python
|
||||||
|
id: setup-python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
artifact_name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
gate:
|
- name: Install uv
|
||||||
name: Docs CI Gate
|
uses: astral-sh/setup-uv@v7
|
||||||
needs: [changes, build]
|
with:
|
||||||
if: always()
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
runs-on: ubuntu-slim
|
enable-cache: true
|
||||||
steps:
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Check gate
|
- name: Install Python dependencies
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ needs.changes.outputs.docs_changed }}" != "true" ]]; then
|
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
|
||||||
echo "No docs-relevant changes detected."
|
- name: Deploy documentation
|
||||||
exit 0
|
run: |
|
||||||
fi
|
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
|
||||||
|
git config --global user.name "${{ github.actor }}"
|
||||||
if [[ "${{ needs.build.result }}" != "success" ]]; then
|
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||||
echo "::error::Docs build job result: ${{ needs.build.result }}"
|
uv run \
|
||||||
exit 1
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
fi
|
--dev \
|
||||||
|
--frozen \
|
||||||
echo "Docs checks passed."
|
mkdocs gh-deploy --force --no-history
|
||||||
|
|||||||
150
.github/workflows/ci-frontend.yml
vendored
150
.github/workflows/ci-frontend.yml
vendored
@@ -3,78 +3,39 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'translations**'
|
- 'translations**'
|
||||||
|
paths:
|
||||||
|
- 'src-ui/**'
|
||||||
|
- '.github/workflows/ci-frontend.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'translations**'
|
- 'translations**'
|
||||||
|
paths:
|
||||||
|
- 'src-ui/**'
|
||||||
|
- '.github/workflows/ci-frontend.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: frontend-${{ github.event.pull_request.number || github.ref }}
|
group: frontend-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
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:
|
install-dependencies:
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
id: cache-frontend-deps
|
id: cache-frontend-deps
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
@@ -84,24 +45,23 @@ jobs:
|
|||||||
run: cd src-ui && pnpm install
|
run: cd src-ui && pnpm install
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
needs: [changes, install-dependencies]
|
needs: install-dependencies
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
@@ -113,8 +73,7 @@ jobs:
|
|||||||
run: cd src-ui && pnpm run lint
|
run: cd src-ui && pnpm run lint
|
||||||
unit-tests:
|
unit-tests:
|
||||||
name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||||
needs: [changes, install-dependencies]
|
needs: install-dependencies
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -124,19 +83,19 @@ jobs:
|
|||||||
shard-count: [4]
|
shard-count: [4]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
@@ -148,22 +107,21 @@ jobs:
|
|||||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
if: always()
|
if: always()
|
||||||
uses: codecov/codecov-action@v5.5.2
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: frontend-node-${{ matrix.node-version }}
|
flags: frontend-node-${{ matrix.node-version }}
|
||||||
directory: src-ui/
|
directory: src-ui/
|
||||||
report_type: test_results
|
report_type: test_results
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5.5.2
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: frontend-node-${{ matrix.node-version }}
|
flags: frontend-node-${{ matrix.node-version }}
|
||||||
directory: src-ui/coverage/
|
directory: src-ui/coverage/
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||||
needs: [changes, install-dependencies]
|
needs: install-dependencies
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
container: mcr.microsoft.com/playwright:v1.58.2-noble
|
container: mcr.microsoft.com/playwright:v1.58.1-noble
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
@@ -175,19 +133,19 @@ jobs:
|
|||||||
shard-count: [2]
|
shard-count: [2]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
@@ -201,26 +159,23 @@ jobs:
|
|||||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
bundle-analysis:
|
bundle-analysis:
|
||||||
name: Bundle Analysis
|
name: Bundle Analysis
|
||||||
needs: [changes, unit-tests, e2e-tests]
|
needs: [unit-tests, e2e-tests]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
@@ -232,42 +187,3 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
run: cd src-ui && pnpm run build --configuration=production
|
run: cd src-ui && pnpm run build --configuration=production
|
||||||
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."
|
|
||||||
|
|||||||
16
.github/workflows/ci-lint.yml
vendored
16
.github/workflows/ci-lint.yml
vendored
@@ -10,15 +10,15 @@ concurrency:
|
|||||||
group: lint-${{ github.event.pull_request.number || github.ref }}
|
group: lint-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
pre-commit:
|
||||||
name: Linting via prek
|
name: Pre-commit Checks
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v6.2.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.11"
|
||||||
- name: Run prek
|
- name: Run pre-commit
|
||||||
uses: j178/prek-action@v1.1.1
|
uses: pre-commit/action@v3.0.1
|
||||||
|
|||||||
36
.github/workflows/ci-release.yml
vendored
36
.github/workflows/ci-release.yml
vendored
@@ -8,15 +8,15 @@ concurrency:
|
|||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.10.x"
|
DEFAULT_UV_VERSION: "0.9.x"
|
||||||
DEFAULT_PYTHON_VERSION: "3.12"
|
DEFAULT_PYTHON_VERSION: "3.11"
|
||||||
jobs:
|
jobs:
|
||||||
wait-for-docker:
|
wait-for-docker:
|
||||||
name: Wait for Docker Build
|
name: Wait for Docker Build
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for Docker build
|
- name: Wait for Docker build
|
||||||
uses: lewagon/wait-on-check-action@v1.5.0
|
uses: lewagon/wait-on-check-action@v1.4.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
check-name: 'Build Docker Image'
|
check-name: 'Build Docker Image'
|
||||||
@@ -28,14 +28,14 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
# ---- Frontend Build ----
|
# ---- Frontend Build ----
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -47,11 +47,11 @@ jobs:
|
|||||||
# ---- Backend Setup ----
|
# ---- Backend Setup ----
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6.2.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7.3.1
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
--frozen \
|
--frozen \
|
||||||
zensical build --clean
|
mkdocs build --config-file ./mkdocs.yml
|
||||||
# ---- Prepare Release ----
|
# ---- Prepare Release ----
|
||||||
- name: Generate requirements file
|
- name: Generate requirements file
|
||||||
run: |
|
run: |
|
||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
sudo chown -R 1000:1000 paperless-ngx/
|
sudo chown -R 1000:1000 paperless-ngx/
|
||||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||||
- name: Upload release artifact
|
- name: Upload release artifact
|
||||||
uses: actions/upload-artifact@v7.0.0
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/paperless-ngx.tar.xz
|
path: dist/paperless-ngx.tar.xz
|
||||||
@@ -133,7 +133,7 @@ jobs:
|
|||||||
version: ${{ steps.get-version.outputs.version }}
|
version: ${{ steps.get-version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download release artifact
|
- name: Download release artifact
|
||||||
uses: actions/download-artifact@v8.0.0
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: ./
|
path: ./
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Create release and changelog
|
- name: Create release and changelog
|
||||||
id: create-release
|
id: create-release
|
||||||
uses: release-drafter/release-drafter@v6.2.0
|
uses: release-drafter/release-drafter@v6
|
||||||
with:
|
with:
|
||||||
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
||||||
tag: ${{ steps.get-version.outputs.version }}
|
tag: ${{ steps.get-version.outputs.version }}
|
||||||
@@ -159,7 +159,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Upload release archive
|
- name: Upload release archive
|
||||||
uses: shogo82148/actions-upload-release-asset@v1.9.2
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||||
@@ -176,16 +176,16 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6.2.0
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7.3.1
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -211,14 +211,14 @@ jobs:
|
|||||||
uv run \
|
uv run \
|
||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
prek run --files changelog.md || true
|
pre-commit run --files changelog.md || true
|
||||||
|
|
||||||
git config --global user.name "github-actions"
|
git config --global user.name "github-actions"
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
|
git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
|
||||||
git push origin ${{ needs.publish-release.outputs.version }}-changelog
|
git push origin ${{ needs.publish-release.outputs.version }}-changelog
|
||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
uses: actions/github-script@v8.0.0
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { repo, owner } = context.repo;
|
const { repo, owner } = context.repo;
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -34,10 +34,10 @@ jobs:
|
|||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4.32.5
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -45,4 +45,4 @@ jobs:
|
|||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4.32.5
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
4
.github/workflows/crowdin.yml
vendored
4
.github/workflows/crowdin.yml
vendored
@@ -13,11 +13,11 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@v2.15.0
|
uses: crowdin/github-action@v2
|
||||||
with:
|
with:
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
|||||||
25
.github/workflows/pr-bot.yml
vendored
25
.github/workflows/pr-bot.yml
vendored
@@ -2,28 +2,17 @@ name: PR Bot
|
|||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened]
|
types: [opened]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
jobs:
|
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:
|
pr-bot:
|
||||||
name: Automated PR Bot
|
name: Automated PR Bot
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Label PR by file path or branch name
|
- name: Label PR by file path or branch name
|
||||||
# see .github/labeler.yml for the labeler config
|
# see .github/labeler.yml for the labeler config
|
||||||
uses: actions/labeler@v6.0.1
|
uses: actions/labeler@v6
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Label by size
|
- name: Label by size
|
||||||
@@ -37,7 +26,7 @@ jobs:
|
|||||||
fail_if_xl: 'false'
|
fail_if_xl: 'false'
|
||||||
excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$
|
excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$
|
||||||
- name: Label by PR title
|
- name: Label by PR title
|
||||||
uses: actions/github-script@v8.0.0
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
@@ -63,7 +52,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
- name: Label bot-generated PRs
|
- name: Label bot-generated PRs
|
||||||
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
|
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
|
||||||
uses: actions/github-script@v8.0.0
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
@@ -88,7 +77,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
- name: Welcome comment
|
- name: Welcome comment
|
||||||
if: ${{ !contains(github.actor, 'bot') }}
|
if: ${{ !contains(github.actor, 'bot') }}
|
||||||
uses: actions/github-script@v8.0.0
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
|
|||||||
2
.github/workflows/project-actions.yml
vendored
2
.github/workflows/project-actions.yml
vendored
@@ -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'
|
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||||
steps:
|
steps:
|
||||||
- name: Label PR with release-drafter
|
- name: Label PR with release-drafter
|
||||||
uses: release-drafter/release-drafter@v6.2.0
|
uses: release-drafter/release-drafter@v6
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
10
.github/workflows/repo-maintenance.yml
vendored
10
.github/workflows/repo-maintenance.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10.2.0
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
days-before-stale: 7
|
days-before-stale: 7
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v6.0.0
|
- uses: dessant/lock-threads@v6
|
||||||
with:
|
with:
|
||||||
issue-inactive-days: '30'
|
issue-inactive-days: '30'
|
||||||
pr-inactive-days: '30'
|
pr-inactive-days: '30'
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v8.0.0
|
- uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v8.0.0
|
- uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
@@ -206,7 +206,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v8.0.0
|
- uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
|
|||||||
14
.github/workflows/translate-strings.yml
vendored
14
.github/workflows/translate-strings.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6
|
||||||
env:
|
env:
|
||||||
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
||||||
with:
|
with:
|
||||||
@@ -19,13 +19,13 @@ jobs:
|
|||||||
ref: ${{ env.GH_REF }}
|
ref: ${{ env.GH_REF }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6.2.0
|
uses: actions/setup-python@v6
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends gettext
|
sudo apt-get install -qq --no-install-recommends gettext
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7.3.1
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Install backend python dependencies
|
- name: Install backend python dependencies
|
||||||
@@ -36,18 +36,18 @@ jobs:
|
|||||||
- name: Generate backend translation strings
|
- name: Generate backend translation strings
|
||||||
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
|
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v6.2.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
id: cache-frontend-deps
|
id: cache-frontend-deps
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
cd src-ui
|
cd src-ui
|
||||||
pnpm run ng extract-i18n
|
pnpm run ng extract-i18n
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: stefanzweifel/git-auto-commit-action@v7.1.0
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
with:
|
with:
|
||||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||||
commit_message: "Auto translate strings"
|
commit_message: "Auto translate strings"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -54,7 +54,7 @@ junit.xml
|
|||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Zensical documentation
|
# MkDocs documentation
|
||||||
site/
|
site/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
|
|||||||
2453
.mypy-baseline.txt
2453
.mypy-baseline.txt
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
# This file configures pre-commit hooks.
|
# This file configures pre-commit hooks.
|
||||||
# See https://pre-commit.com/ for general information
|
# See https://pre-commit.com/ for general information
|
||||||
# See https://pre-commit.com/hooks.html for a listing of possible hooks
|
# See https://pre-commit.com/hooks.html for a listing of possible hooks
|
||||||
# We actually run via https://github.com/j178/prek which is compatible
|
|
||||||
repos:
|
repos:
|
||||||
# General hooks
|
# General hooks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
@@ -50,12 +49,12 @@ repos:
|
|||||||
- 'prettier-plugin-organize-imports@4.1.0'
|
- 'prettier-plugin-organize-imports@4.1.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.5
|
rev: v0.14.14
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.12.1"
|
rev: "v2.11.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
# Dockerfile hooks
|
# Dockerfile hooks
|
||||||
|
|||||||
17368
.pyrefly-baseline.json
17368
.pyrefly-baseline.json
File diff suppressed because one or more lines are too long
@@ -13,9 +13,7 @@ If you want to implement something big:
|
|||||||
|
|
||||||
## Python
|
## 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.
|
Paperless supports python 3.10 - 3.12 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
|
||||||
|
|
||||||
We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
|
|
||||||
|
|
||||||
## Branches
|
## Branches
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.10.9-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.9.26-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ ENV \
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
# Lock this version
|
# 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 \
|
ARG S6_BUILD_TIME_PKGS="curl \
|
||||||
xz-utils"
|
xz-utils"
|
||||||
@@ -154,6 +154,8 @@ ARG RUNTIME_PACKAGES="\
|
|||||||
libmagic1 \
|
libmagic1 \
|
||||||
media-types \
|
media-types \
|
||||||
zlib1g \
|
zlib1g \
|
||||||
|
# Barcode splitter
|
||||||
|
libzbar0 \
|
||||||
poppler-utils"
|
poppler-utils"
|
||||||
|
|
||||||
# Install basic runtime packages.
|
# Install basic runtime packages.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# correct networking for the tests
|
# correct networking for the tests
|
||||||
services:
|
services:
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.27
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
hostname: gotenberg
|
hostname: gotenberg
|
||||||
container_name: gotenberg
|
container_name: gotenberg
|
||||||
network_mode: host
|
network_mode: host
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- "3143:3143" # IMAP
|
- "3143:3143" # IMAP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
nginx:
|
nginx:
|
||||||
image: docker.io/nginx:1.29.5-alpine
|
image: docker.io/nginx:1.29-alpine
|
||||||
hostname: nginx
|
hostname: nginx
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.27
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
PAPERLESS_DBENGINE: postgres
|
|
||||||
env_file:
|
env_file:
|
||||||
- stack.env
|
- stack.env
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -62,12 +62,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
PAPERLESS_DBENGINE: postgresql
|
|
||||||
PAPERLESS_TIKA_ENABLED: 1
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.27
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
PAPERLESS_DBENGINE: postgresql
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
|||||||
@@ -51,12 +51,11 @@ services:
|
|||||||
env_file: docker-compose.env
|
env_file: docker-compose.env
|
||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBENGINE: sqlite
|
|
||||||
PAPERLESS_TIKA_ENABLED: 1
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.27
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ services:
|
|||||||
env_file: docker-compose.env
|
env_file: docker-compose.env
|
||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBENGINE: sqlite
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
|||||||
@@ -10,12 +10,8 @@ cd "${PAPERLESS_SRC_DIR}"
|
|||||||
|
|
||||||
# The whole migrate, with flock, needs to run as the right user
|
# The whole migrate, with flock, needs to run as the right user
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py check --tag compatibility paperless
|
|
||||||
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
||||||
else
|
else
|
||||||
exec s6-setuidgid paperless \
|
|
||||||
s6-setlock -n "${data_dir}/migration_lock" \
|
|
||||||
python3 manage.py check --tag compatibility paperless
|
|
||||||
exec s6-setuidgid paperless \
|
exec s6-setuidgid paperless \
|
||||||
s6-setlock -n "${data_dir}/migration_lock" \
|
s6-setlock -n "${data_dir}/migration_lock" \
|
||||||
python3 manage.py migrate --skip-checks --no-input
|
python3 manage.py migrate --skip-checks --no-input
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ copies you created in the steps above.
|
|||||||
|
|
||||||
## Updating Paperless {#updating}
|
## 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}
|
### Docker Route {#docker-updating}
|
||||||
|
|
||||||
If a new release of paperless-ngx is available, upgrading depends on how
|
If a new release of paperless-ngx is available, upgrading depends on how
|
||||||
|
|||||||
@@ -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
|
or using [storage paths (see below)](#storage-paths). Paperless adds the
|
||||||
correct file extension e.g. `.pdf`, `.jpg` automatically.
|
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)
|
This variable allows you to configure the filename (folders are allowed)
|
||||||
using placeholders. For example, configuring this to
|
using placeholders. For example, configuring this to
|
||||||
|
|
||||||
@@ -357,8 +353,6 @@ If paperless detects that two documents share the same filename,
|
|||||||
paperless will automatically append `_01`, `_02`, etc to the filename.
|
paperless will automatically append `_01`, `_02`, etc to the filename.
|
||||||
This happens if all the placeholders in a filename evaluate to the same
|
This happens if all the placeholders in a filename evaluate to the same
|
||||||
value.
|
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`,
|
If there are any errors in the placeholders included in `PAPERLESS_FILENAME_FORMAT`,
|
||||||
paperless will fall back to using the default naming scheme instead.
|
paperless will fall back to using the default naming scheme instead.
|
||||||
@@ -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)
|
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables)
|
||||||
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
|
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
|
||||||
|
|
||||||
In addition, a limited `document` object is available for advanced templates.
|
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
|
||||||
This object includes common metadata fields such as `id`, `pk`, `title`, `content`, `page_count`, `created`, `added`, `modified`, `mime_type`,
|
with more complex logic.
|
||||||
`checksum`, `archive_checksum`, `archive_serial_number`, `filename`, `archive_filename`, and `original_filename`.
|
|
||||||
Related values are available as nested objects with limited fields, for example document.correspondent.name, etc.
|
|
||||||
|
|
||||||
#### Custom Jinja2 Filters
|
#### Custom Jinja2 Filters
|
||||||
|
|
||||||
@@ -782,6 +774,7 @@ At this time, the library utilized for detection of barcodes supports the follow
|
|||||||
- QR Code
|
- QR Code
|
||||||
- SQ 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 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).
|
For how to enable barcode usage, see [the configuration](configuration.md#barcodes).
|
||||||
@@ -790,17 +783,9 @@ below.
|
|||||||
|
|
||||||
### Document Splitting {#document-splitting}
|
### Document Splitting {#document-splitting}
|
||||||
|
|
||||||
If document splitting is enabled, Paperless splits _after_ a separator barcode by default.
|
When enabled, Paperless will look for a barcode with the configured value and create a new document
|
||||||
This means:
|
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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
### Archive Serial Number Assignment
|
### 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
|
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 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
|
barcode _will_ be retained. This allows application of a barcode to any page, including
|
||||||
that page_. This allows placing ASN barcodes on content pages that should remain part of
|
one which holds data to keep in the document.
|
||||||
the document.
|
|
||||||
|
|
||||||
### Tag Assignment
|
### Tag Assignment
|
||||||
|
|
||||||
|
|||||||
100
docs/api.md
100
docs/api.md
@@ -1,4 +1,4 @@
|
|||||||
# REST API
|
# The REST API
|
||||||
|
|
||||||
Paperless-ngx now ships with a fully-documented REST API and a browsable
|
Paperless-ngx now ships with a fully-documented REST API and a browsable
|
||||||
web interface to explore it. The API browsable interface is available at
|
web interface to explore it. The API browsable interface is available at
|
||||||
@@ -211,21 +211,6 @@ However, querying the tasks endpoint with the returned UUID e.g.
|
|||||||
`/api/tasks/?task_id={uuid}` will provide information on the state of the
|
`/api/tasks/?task_id={uuid}` will provide information on the state of the
|
||||||
consumption including the ID of a created document if consumption succeeded.
|
consumption including the ID of a created document if consumption succeeded.
|
||||||
|
|
||||||
## Document Versions
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
All objects (documents, tags, etc.) allow setting object-level permissions
|
All objects (documents, tags, etc.) allow setting object-level permissions
|
||||||
@@ -305,16 +290,52 @@ The following methods are supported:
|
|||||||
- `"merge": true or false` (defaults to false)
|
- `"merge": true or false` (defaults to false)
|
||||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||||
removing them) or be merged with existing permissions.
|
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`
|
- `modify_custom_fields`
|
||||||
- Requires `parameters`:
|
- 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
|
- `"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.
|
to add with empty values.
|
||||||
- `"remove_custom_fields": [CUSTOM_FIELD_ID]`: custom field ids to remove from the document.
|
- `"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.
|
|
||||||
|
|
||||||
### Objects
|
### Objects
|
||||||
|
|
||||||
Bulk editing for objects (tags, document types etc.) currently supports set permissions or delete
|
Bulk editing for objects (tags, document types etc.) currently supports set permissions or delete
|
||||||
@@ -333,38 +354,41 @@ operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json
|
|||||||
|
|
||||||
## API Versioning
|
## 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
|
- Versioning ensures that changes to the API don't break older
|
||||||
clients.
|
clients.
|
||||||
- Clients specify the specific version of the API they wish to use
|
- Clients specify the specific version of the API they wish to use
|
||||||
with every request and Paperless will handle the request using the
|
with every request and Paperless will handle the request using the
|
||||||
specified API version.
|
specified API version.
|
||||||
- Even if the underlying data model changes, supported older API
|
- Even if the underlying data model changes, older API versions will
|
||||||
versions continue to serve compatible data.
|
always serve compatible data.
|
||||||
- If no version is specified, Paperless serves the configured default
|
- If no version is specified, Paperless will serve version 1 to ensure
|
||||||
API version (currently `10`).
|
compatibility with older clients that do not request a specific API
|
||||||
- Supported API versions are currently `9` and `10`.
|
version.
|
||||||
|
|
||||||
API versions are specified by submitting an additional HTTP `Accept`
|
API versions are specified by submitting an additional HTTP `Accept`
|
||||||
header with every request:
|
header with every request:
|
||||||
|
|
||||||
```
|
```
|
||||||
Accept: application/json; version=10
|
Accept: application/json; version=6
|
||||||
```
|
```
|
||||||
|
|
||||||
If an invalid version is specified, Paperless responds with
|
If an invalid version is specified, Paperless 1.3.0 will respond with
|
||||||
`406 Not Acceptable` and an error message in the body.
|
"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
|
If a client wishes to verify whether it is compatible with any given
|
||||||
server, the following procedure should be performed:
|
server, the following procedure should be performed:
|
||||||
|
|
||||||
1. Perform an _authenticated_ request against any API endpoint. The
|
1. Perform an _authenticated_ request against any API endpoint. If the
|
||||||
server will add two custom headers to the response:
|
server is on version 1.3.0 or newer, the server will add two custom
|
||||||
|
headers to the response:
|
||||||
|
|
||||||
```
|
```
|
||||||
X-Api-Version: 10
|
X-Api-Version: 2
|
||||||
X-Version: <server-version>
|
X-Version: 1.3.0
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Determine whether the client is compatible with this server based on
|
2. Determine whether the client is compatible with this server based on
|
||||||
@@ -427,13 +451,3 @@ Initial API version.
|
|||||||
- The document `created` field is now a date, not a datetime. The
|
- The document `created` field is now a date, not a datetime. The
|
||||||
`created_date` field is considered deprecated and will be removed in a
|
`created_date` field is considered deprecated and will be removed in a
|
||||||
future version.
|
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.
|
|
||||||
|
|||||||
@@ -1,31 +1,13 @@
|
|||||||
:root>* {
|
:root > * {
|
||||||
--paperless-green: #17541f;
|
--md-primary-fg-color: #17541f;
|
||||||
--paperless-green-accent: #2b8a38;
|
--md-primary-fg-color--dark: #17541f;
|
||||||
--md-primary-fg-color: var(--paperless-green);
|
--md-primary-fg-color--light: #17541f;
|
||||||
--md-primary-fg-color--dark: var(--paperless-green);
|
--md-accent-fg-color: #2b8a38;
|
||||||
--md-primary-fg-color--light: var(--paperless-green-accent);
|
|
||||||
--md-accent-fg-color: var(--paperless-green-accent);
|
|
||||||
--md-typeset-a-color: #21652a;
|
--md-typeset-a-color: #21652a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-header,
|
|
||||||
.md-tabs {
|
|
||||||
background-color: var(--paperless-green);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-tabs__link {
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-tabs__link:hover,
|
|
||||||
.md-tabs__link--active {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-md-color-scheme="slate"] {
|
[data-md-color-scheme="slate"] {
|
||||||
--md-hue: 222;
|
--md-hue: 222;
|
||||||
--md-default-bg-color: hsla(var(--md-hue), 15%, 10%, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -87,8 +69,8 @@ h4 code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
||||||
.md-nav.md-nav--secondary .md-nav__item:has(> .md-nav__link[href*="PAPERLESS_"]),
|
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="PAPERLESS_"],
|
||||||
.md-nav.md-nav--secondary .md-nav__item:has(> .md-nav__link[href*="USERMAP_"]) {
|
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="USERMAP_"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,3 +83,18 @@ h4 code {
|
|||||||
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Show search shortcut key */
|
||||||
|
[data-md-toggle="search"]:not(:checked) ~ .md-header .md-search__form::after {
|
||||||
|
position: absolute;
|
||||||
|
top: .3rem;
|
||||||
|
right: .3rem;
|
||||||
|
display: block;
|
||||||
|
padding: .1rem .4rem;
|
||||||
|
color: var(--md-default-fg-color--lighter);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: .8rem;
|
||||||
|
border: .05rem solid var(--md-default-fg-color--lighter);
|
||||||
|
border-radius: .1rem;
|
||||||
|
content: "/";
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
@@ -1,77 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## paperless-ngx 2.20.10
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Fix: support string coercion in filepath jinja templates [@shamoon](https://github.com/shamoon) ([#12244](https://github.com/paperless-ngx/paperless-ngx/pull/12244))
|
|
||||||
- Fix: apply ordering after annotating tag document count [@shamoon](https://github.com/shamoon) ([#12238](https://github.com/paperless-ngx/paperless-ngx/pull/12238))
|
|
||||||
- Fix: enforce path limit for db filename fields [@shamoon](https://github.com/shamoon) ([#12235](https://github.com/paperless-ngx/paperless-ngx/pull/12235))
|
|
||||||
|
|
||||||
### All App Changes
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>3 changes</summary>
|
|
||||||
|
|
||||||
- Fix: support string coercion in filepath jinja templates [@shamoon](https://github.com/shamoon) ([#12244](https://github.com/paperless-ngx/paperless-ngx/pull/12244))
|
|
||||||
- Fix: apply ordering after annotating tag document count [@shamoon](https://github.com/shamoon) ([#12238](https://github.com/paperless-ngx/paperless-ngx/pull/12238))
|
|
||||||
- Fix: enforce path limit for db filename fields [@shamoon](https://github.com/shamoon) ([#12235](https://github.com/paperless-ngx/paperless-ngx/pull/12235))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## paperless-ngx 2.20.9
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-386h-chg4-cfw9](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-386h-chg4-cfw9)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Fixhancement: config option reset [@shamoon](https://github.com/shamoon) ([#12176](https://github.com/paperless-ngx/paperless-ngx/pull/12176))
|
|
||||||
- Fix: correct page count by separating display vs collection sizes for tags [@shamoon](https://github.com/shamoon) ([#12170](https://github.com/paperless-ngx/paperless-ngx/pull/12170))
|
|
||||||
|
|
||||||
### All App Changes
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>2 changes</summary>
|
|
||||||
|
|
||||||
- Fixhancement: config option reset [@shamoon](https://github.com/shamoon) ([#12176](https://github.com/paperless-ngx/paperless-ngx/pull/12176))
|
|
||||||
- Fix: correct page count by separating display vs collection sizes for tags [@shamoon](https://github.com/shamoon) ([#12170](https://github.com/paperless-ngx/paperless-ngx/pull/12170))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## paperless-ngx 2.20.8
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-7qqc-wrcw-2fj9](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7qqc-wrcw-2fj9)
|
|
||||||
|
|
||||||
## paperless-ngx 2.20.7
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-x395-6h48-wr8v](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-x395-6h48-wr8v)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Performance fix: use subqueries to improve object retrieval in large installs [@shamoon](https://github.com/shamoon) ([#11950](https://github.com/paperless-ngx/paperless-ngx/pull/11950))
|
|
||||||
- Fix: correct user dropdown button icon styling [@shamoon](https://github.com/shamoon) ([#12092](https://github.com/paperless-ngx/paperless-ngx/issues/12092))
|
|
||||||
- Fix: fix broken docker create_classifier command in 2.20.6 [@shamoon](https://github.com/shamoon) ([#11965](https://github.com/paperless-ngx/paperless-ngx/issues/11965))
|
|
||||||
|
|
||||||
### All App Changes
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>3 changes</summary>
|
|
||||||
|
|
||||||
- Performance fix: use subqueries to improve object retrieval in large installs [@shamoon](https://github.com/shamoon) ([#11950](https://github.com/paperless-ngx/paperless-ngx/pull/11950))
|
|
||||||
- Fix: correct user dropdown button icon styling [@shamoon](https://github.com/shamoon) ([#12092](https://github.com/paperless-ngx/paperless-ngx/issues/12092))
|
|
||||||
- Fix: fix broken docker create_classifier command in 2.20.6 [@shamoon](https://github.com/shamoon) ([#11965](https://github.com/paperless-ngx/paperless-ngx/issues/11965))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## paperless-ngx 2.20.6
|
## paperless-ngx 2.20.6
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-jqwv-hx7q-fxh3](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-jqwv-hx7q-fxh3) and [GHSA-w47q-3m69-84v8](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-w47q-3m69-84v8)
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888))
|
- Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888))
|
||||||
|
|||||||
@@ -51,172 +51,137 @@ matcher.
|
|||||||
### Database
|
### Database
|
||||||
|
|
||||||
By default, Paperless uses **SQLite** with a database stored at `data/db.sqlite3`.
|
By default, Paperless uses **SQLite** with a database stored at `data/db.sqlite3`.
|
||||||
For multi-user or higher-throughput deployments, **PostgreSQL** (recommended) or
|
To switch to **PostgreSQL** or **MariaDB**, set [`PAPERLESS_DBHOST`](#PAPERLESS_DBHOST) and optionally configure other
|
||||||
**MariaDB** can be used instead by setting [`PAPERLESS_DBENGINE`](#PAPERLESS_DBENGINE)
|
database-related environment variables.
|
||||||
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).
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DBHOST=<hostname>`](#PAPERLESS_DBHOST) {#PAPERLESS_DBHOST}
|
#### [`PAPERLESS_DBHOST=<hostname>`](#PAPERLESS_DBHOST) {#PAPERLESS_DBHOST}
|
||||||
|
|
||||||
: Hostname of the PostgreSQL or MariaDB database server. Required when
|
: If unset, Paperless uses **SQLite** by default.
|
||||||
`PAPERLESS_DBENGINE` is `postgresql` or `mariadb`.
|
|
||||||
|
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}
|
#### [`PAPERLESS_DBPORT=<port>`](#PAPERLESS_DBPORT) {#PAPERLESS_DBPORT}
|
||||||
|
|
||||||
: Port to use when connecting to PostgreSQL or MariaDB.
|
: 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}
|
#### [`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.
|
: Username for authenticating with the PostgreSQL or MariaDB database.
|
||||||
|
|
||||||
Defaults to `paperless`.
|
Defaults to "paperless".
|
||||||
|
|
||||||
#### [`PAPERLESS_DBPASS=<password>`](#PAPERLESS_DBPASS) {#PAPERLESS_DBPASS}
|
#### [`PAPERLESS_DBPASS=<password>`](#PAPERLESS_DBPASS) {#PAPERLESS_DBPASS}
|
||||||
|
|
||||||
: Password for the PostgreSQL or MariaDB database user.
|
: 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.
|
: SSL mode to use when connecting to PostgreSQL or MariaDB.
|
||||||
Keys and values are separated by `=`. Dot-notation produces nested option
|
|
||||||
dictionaries; for example, `pool.max_size=20` sets
|
|
||||||
`OPTIONS["pool"]["max_size"] = 20`.
|
|
||||||
|
|
||||||
Options specified here are merged over the engine defaults. Unrecognised keys
|
See [the official documentation about
|
||||||
are passed through to the underlying database driver without validation, so a
|
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||||
typo will be silently ignored rather than producing an error.
|
|
||||||
|
|
||||||
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)
|
*Note*: SSL mode values differ between PostgreSQL and MariaDB.
|
||||||
- MariaDB: [MariaDB Connector/Python](https://mariadb.com/kb/en/mariadb-connector-python/)
|
|
||||||
- SQLite: [SQLite PRAGMA statements](https://www.sqlite.org/pragma.html)
|
|
||||||
|
|
||||||
!!! 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
|
#### [`PAPERLESS_DBSSLROOTCERT=<ca-path>`](#PAPERLESS_DBSSLROOTCERT) {#PAPERLESS_DBSSLROOTCERT}
|
||||||
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`.
|
|
||||||
|
|
||||||
**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"
|
See [the official documentation about
|
||||||
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=5"
|
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"
|
See [the official documentation about
|
||||||
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED;ssl.ca=/certs/ca.pem"
|
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"
|
Defaults to unset, using the standard location in the home directory.
|
||||||
# PostgreSQL: set a connection timeout
|
|
||||||
PAPERLESS_DB_OPTIONS="connect_timeout=10"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ~~[`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"
|
See [the official documentation about
|
||||||
PAPERLESS_DB_OPTIONS="sslmode=require"
|
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-cert).
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="MariaDB"
|
Changes the location of `postgresql.crt`.
|
||||||
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ~~[`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"
|
See [the official documentation about
|
||||||
PAPERLESS_DB_OPTIONS="sslrootcert=/path/to/ca.pem"
|
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="MariaDB"
|
See [the official documentation about
|
||||||
PAPERLESS_DB_OPTIONS="ssl.ca=/path/to/ca.pem"
|
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"
|
: Sets how long a database connection should wait before timing out.
|
||||||
PAPERLESS_DB_OPTIONS="sslcert=/path/to/client.crt"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="MariaDB"
|
For SQLite, this sets how long to wait if the database is locked.
|
||||||
PAPERLESS_DB_OPTIONS="ssl.cert=/path/to/client.crt"
|
For PostgreSQL or MariaDB, this sets the connection timeout.
|
||||||
```
|
|
||||||
|
|
||||||
#### ~~[`PAPERLESS_DBSSLKEY`](#PAPERLESS_DBSSLKEY)~~ {#PAPERLESS_DBSSLKEY}
|
Defaults to unset, which uses Django’s 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"
|
Only applies to PostgreSQL. This setting is ignored for other database engines.
|
||||||
PAPERLESS_DB_OPTIONS="sslkey=/path/to/client.key"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="MariaDB"
|
The value must be greater than or equal to 1 to be used.
|
||||||
PAPERLESS_DB_OPTIONS="ssl.key=/path/to/client.key"
|
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"
|
This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications,
|
||||||
PAPERLESS_DB_OPTIONS="timeout=30"
|
you should increase `max_connections` accordingly.
|
||||||
```
|
|
||||||
|
|
||||||
```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"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
||||||
|
|
||||||
@@ -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.
|
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}
|
#### [`PAPERLESS_PRE_CONSUME_SCRIPT=<filename>`](#PAPERLESS_PRE_CONSUME_SCRIPT) {#PAPERLESS_PRE_CONSUME_SCRIPT}
|
||||||
|
|
||||||
: After some initial validation, Paperless can trigger an arbitrary
|
: After some initial validation, Paperless can trigger an arbitrary
|
||||||
|
|||||||
@@ -75,13 +75,13 @@ first-time setup.
|
|||||||
4. Install the Python dependencies:
|
4. Install the Python dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync --group dev
|
$ uv sync --group dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Install pre-commit hooks:
|
5. Install pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run prek install
|
$ uv run pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
|
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
|
||||||
@@ -89,8 +89,8 @@ first-time setup.
|
|||||||
```bash
|
```bash
|
||||||
# src/
|
# src/
|
||||||
|
|
||||||
uv run manage.py migrate
|
$ uv run manage.py migrate
|
||||||
uv run manage.py createsuperuser
|
$ uv run manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
7. You can now either ...
|
7. You can now either ...
|
||||||
@@ -103,7 +103,7 @@ first-time setup.
|
|||||||
|
|
||||||
- spin up a bare Redis container
|
- spin up a bare Redis container
|
||||||
|
|
||||||
```bash
|
```
|
||||||
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,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 IDE to use the `src/`-folder as the base source folder.
|
||||||
Configure the following launch configurations in your IDE:
|
Configure the following launch configurations in your IDE:
|
||||||
|
|
||||||
- `uv run manage.py runserver`
|
- `python3 manage.py runserver`
|
||||||
- `uv run manage.py document_consumer`
|
- `python3 manage.py document_consumer`
|
||||||
- `uv run celery --app paperless worker -l DEBUG` (or any other log level)
|
- `celery --app paperless worker -l DEBUG` (or any other log level)
|
||||||
|
|
||||||
To start them all:
|
To start them all:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# src/
|
# src/
|
||||||
|
|
||||||
uv run manage.py runserver & \
|
$ python3 manage.py runserver & \
|
||||||
uv run manage.py document_consumer & \
|
python3 manage.py document_consumer & \
|
||||||
uv run celery --app paperless worker -l DEBUG
|
celery --app paperless worker -l DEBUG
|
||||||
```
|
```
|
||||||
|
|
||||||
You might need the front end to test your back end code.
|
You might need the front end to test your back end code.
|
||||||
@@ -140,8 +140,8 @@ To build the front end once use this command:
|
|||||||
```bash
|
```bash
|
||||||
# src-ui/
|
# src-ui/
|
||||||
|
|
||||||
pnpm install
|
$ pnpm install
|
||||||
pnpm ng build --configuration production
|
$ ng build --configuration production
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
@@ -199,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:
|
4. You can launch a development server by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm ng serve
|
ng serve
|
||||||
```
|
```
|
||||||
|
|
||||||
This will automatically update whenever you save. However, in-place
|
This will automatically update whenever you save. However, in-place
|
||||||
@@ -217,21 +217,21 @@ commit. See [above](#code-formatting-with-pre-commit-hooks) for installation ins
|
|||||||
command such as
|
command such as
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git ls-files -- '*.ts' | xargs uv run prek run prettier --files
|
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||||
```
|
```
|
||||||
|
|
||||||
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
||||||
respectively, can be run non-interactively with:
|
respectively, can be run non-interactively with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm ng test
|
$ ng test
|
||||||
pnpm playwright test
|
$ npx playwright test
|
||||||
```
|
```
|
||||||
|
|
||||||
Playwright also includes a UI which can be run with:
|
Playwright also includes a UI which can be run with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm playwright test --ui
|
$ npx playwright test --ui
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building the frontend
|
### Building the frontend
|
||||||
@@ -239,7 +239,7 @@ pnpm playwright test --ui
|
|||||||
In order to build the front end and serve it as part of Django, execute:
|
In order to build the front end and serve it as part of Django, execute:
|
||||||
|
|
||||||
```bash
|
```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
|
This will build the front end and put it in a location from which the
|
||||||
@@ -312,10 +312,10 @@ end (such as error messages).
|
|||||||
- The source language of the project is "en_US".
|
- The source language of the project is "en_US".
|
||||||
- Localization files end up in the folder `src/locale/`.
|
- Localization files end up in the folder `src/locale/`.
|
||||||
- In order to extract strings from the application, call
|
- In order to extract strings from the application, call
|
||||||
`uv run manage.py makemessages -l en_US`. This is important after
|
`python3 manage.py makemessages -l en_US`. This is important after
|
||||||
making changes to translatable strings.
|
making changes to translatable strings.
|
||||||
- The message files need to be compiled for them to show up in the
|
- The message files need to be compiled for them to show up in the
|
||||||
application. Call `uv run manage.py compilemessages` to do this.
|
application. Call `python3 manage.py compilemessages` to do this.
|
||||||
The generated files don't get committed into git, since these are
|
The generated files don't get committed into git, since these are
|
||||||
derived artifacts. The build pipeline takes care of executing this
|
derived artifacts. The build pipeline takes care of executing this
|
||||||
command.
|
command.
|
||||||
@@ -338,13 +338,13 @@ LANGUAGES = [
|
|||||||
|
|
||||||
## Building the documentation
|
## Building the documentation
|
||||||
|
|
||||||
The documentation is built using Zensical, see their [documentation](https://zensical.org/docs/).
|
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
||||||
If you want to build the documentation locally, this is how you do it:
|
If you want to build the documentation locally, this is how you do it:
|
||||||
|
|
||||||
1. Build the documentation
|
1. Build the documentation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run zensical build
|
$ uv run mkdocs build --config-file mkdocs.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
_alternatively..._
|
_alternatively..._
|
||||||
@@ -355,10 +355,10 @@ If you want to build the documentation locally, this is how you do it:
|
|||||||
something.
|
something.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run zensical serve
|
$ uv run mkdocs serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the Docker image {#docker_build}
|
## Building the Docker image
|
||||||
|
|
||||||
The docker image is primarily built by the GitHub actions workflow, but
|
The docker image is primarily built by the GitHub actions workflow, but
|
||||||
it can be faster when developing to build and tag an image locally.
|
it can be faster when developing to build and tag an image locally.
|
||||||
@@ -481,147 +481,3 @@ To get started:
|
|||||||
|
|
||||||
5. The project is ready for debugging, start either run the fullstack debug or individual debug
|
5. The project is ready for debugging, start either run the fullstack debug or individual debug
|
||||||
processes. Yo spin up the project without debugging run the task **Project Start: Run all Services**
|
processes. Yo spin up the project without debugging run the task **Project Start: Run all Services**
|
||||||
|
|
||||||
## Developing Date Parser Plugins
|
|
||||||
|
|
||||||
Paperless-ngx uses a plugin system for date parsing, allowing you to extend or replace the default date parsing behavior. Plugins are discovered using [Python entry points](https://setuptools.pypa.io/en/latest/userguide/entry_point.html).
|
|
||||||
|
|
||||||
### Creating a Date Parser Plugin
|
|
||||||
|
|
||||||
To create a custom date parser plugin, you need to:
|
|
||||||
|
|
||||||
1. Create a class that inherits from `DateParserPluginBase`
|
|
||||||
2. Implement the required abstract method
|
|
||||||
3. Register your plugin via an entry point
|
|
||||||
|
|
||||||
#### 1. Implementing the Parser Class
|
|
||||||
|
|
||||||
Your parser must extend `documents.plugins.date_parsing.DateParserPluginBase` and implement the `parse` method:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from collections.abc import Iterator
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from documents.plugins.date_parsing import DateParserPluginBase
|
|
||||||
|
|
||||||
|
|
||||||
class MyDateParserPlugin(DateParserPluginBase):
|
|
||||||
"""
|
|
||||||
Custom date parser implementation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def parse(self, filename: str, content: str) -> Iterator[datetime.datetime]:
|
|
||||||
"""
|
|
||||||
Parse dates from the document's filename and content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The original filename of the document
|
|
||||||
content: The extracted text content of the document
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
datetime.datetime: Valid datetime objects found in the document
|
|
||||||
"""
|
|
||||||
# Your parsing logic here
|
|
||||||
# Use self.config to access configuration settings
|
|
||||||
|
|
||||||
# Example: parse dates from filename first
|
|
||||||
if self.config.filename_date_order:
|
|
||||||
# Your filename parsing logic
|
|
||||||
yield some_datetime
|
|
||||||
|
|
||||||
# Then parse dates from content
|
|
||||||
# Your content parsing logic
|
|
||||||
yield another_datetime
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Configuration and Helper Methods
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
The base class provides two helper methods you can use:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _parse_string(
|
|
||||||
self,
|
|
||||||
date_string: str,
|
|
||||||
date_order: str,
|
|
||||||
) -> datetime.datetime | None:
|
|
||||||
"""
|
|
||||||
Parse a single date string using dateparser with configured settings.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _filter_date(
|
|
||||||
self,
|
|
||||||
date: datetime.datetime | None,
|
|
||||||
) -> datetime.datetime | None:
|
|
||||||
"""
|
|
||||||
Validate a parsed datetime against configured rules.
|
|
||||||
Filters out dates before 1900, future dates, and ignored dates.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Resource Management (Optional)
|
|
||||||
|
|
||||||
If your plugin needs to acquire or release resources (database connections, API clients, etc.), override the context manager methods. Paperless-ngx will always use plugins as context managers, ensuring resources can be released even in the event of errors.
|
|
||||||
|
|
||||||
#### 4. Registering Your Plugin
|
|
||||||
|
|
||||||
Register your plugin using a setuptools entry point in your package's `pyproject.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[project.entry-points."paperless_ngx.date_parsers"]
|
|
||||||
my_parser = "my_package.parsers:MyDateParserPlugin"
|
|
||||||
```
|
|
||||||
|
|
||||||
The entry point name (e.g., `"my_parser"`) is used for sorting when multiple plugins are found. Paperless-ngx will use the first plugin alphabetically by name if multiple plugins are discovered.
|
|
||||||
|
|
||||||
### Plugin Discovery
|
|
||||||
|
|
||||||
Paperless-ngx automatically discovers and loads date parser plugins at runtime. The discovery process:
|
|
||||||
|
|
||||||
1. Queries the `paperless_ngx.date_parsers` entry point group
|
|
||||||
2. Validates that each plugin is a subclass of `DateParserPluginBase`
|
|
||||||
3. Sorts valid plugins alphabetically by entry point name
|
|
||||||
4. Uses the first valid plugin, or falls back to the default `RegexDateParserPlugin` if none are found
|
|
||||||
|
|
||||||
If multiple plugins are installed, a warning is logged indicating which plugin was selected.
|
|
||||||
|
|
||||||
### Example: Simple Date Parser
|
|
||||||
|
|
||||||
Here's a minimal example that only looks for ISO 8601 dates:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
from collections.abc import Iterator
|
|
||||||
|
|
||||||
from documents.plugins.date_parsing.base import DateParserPluginBase
|
|
||||||
|
|
||||||
|
|
||||||
class ISODateParserPlugin(DateParserPluginBase):
|
|
||||||
"""
|
|
||||||
Parser that only matches ISO 8601 formatted dates (YYYY-MM-DD).
|
|
||||||
"""
|
|
||||||
|
|
||||||
ISO_REGEX = re.compile(r"\b(\d{4}-\d{2}-\d{2})\b")
|
|
||||||
|
|
||||||
def parse(self, filename: str, content: str) -> Iterator[datetime.datetime]:
|
|
||||||
# Combine filename and content for searching
|
|
||||||
text = f"{filename} {content}"
|
|
||||||
|
|
||||||
for match in self.ISO_REGEX.finditer(text):
|
|
||||||
date_string = match.group(1)
|
|
||||||
# Use helper method to parse with configured timezone
|
|
||||||
date = self._parse_string(date_string, "YMD")
|
|
||||||
# Use helper method to validate the date
|
|
||||||
filtered_date = self._filter_date(date)
|
|
||||||
if filtered_date is not None:
|
|
||||||
yield filtered_date
|
|
||||||
```
|
|
||||||
|
|||||||
10
docs/faq.md
10
docs/faq.md
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
title: FAQs
|
|
||||||
---
|
|
||||||
|
|
||||||
# Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
## _What's the general plan for Paperless-ngx?_
|
## _What's the general plan for Paperless-ngx?_
|
||||||
@@ -67,10 +63,8 @@ elsewhere. Here are a couple notes about that.
|
|||||||
Paperless also supports various Office documents (.docx, .doc, odt,
|
Paperless also supports various Office documents (.docx, .doc, odt,
|
||||||
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
||||||
|
|
||||||
Paperless-ngx determines the type of a file by inspecting its content
|
Paperless-ngx determines the type of a file by inspecting its content.
|
||||||
rather than its file extensions. However, files processed via the
|
The file extensions do not matter.
|
||||||
consumption directory will be rejected if they have a file extension that
|
|
||||||
not supported by any of the available parsers.
|
|
||||||
|
|
||||||
## _Will paperless-ngx run on Raspberry Pi?_
|
## _Will paperless-ngx run on Raspberry Pi?_
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
title: Home
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="grid-left" markdown>
|
<div class="grid-left" markdown>
|
||||||
{.index-logo}
|
{.index-logo}
|
||||||
{.index-logo}
|
{.index-logo}
|
||||||
|
|||||||
@@ -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
25
docs/migration.md
Normal 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.
|
||||||
568
docs/setup.md
568
docs/setup.md
@@ -1,77 +1,52 @@
|
|||||||
---
|
## Installation
|
||||||
title: Setup
|
|
||||||
---
|
|
||||||
|
|
||||||
# Installation
|
You can go multiple routes to setup and run Paperless:
|
||||||
|
|
||||||
!!! tip "Quick Start"
|
- [Use the script to setup a Docker install](#docker_script)
|
||||||
|
- [Use the Docker compose templates](#docker)
|
||||||
|
- [Build the Docker image yourself](#docker_build)
|
||||||
|
- [Install Paperless-ngx directly on your system manually ("bare metal")](#bare_metal)
|
||||||
|
- A user-maintained list of commercial hosting providers can be found [in the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects)
|
||||||
|
|
||||||
|
The Docker routes are quick & easy. These are the recommended routes.
|
||||||
|
This configures all the stuff from the above automatically so that it
|
||||||
|
just works and uses sensible defaults for all configuration options.
|
||||||
|
Here you find a cheat-sheet for docker beginners: [CLI
|
||||||
|
Basics](https://www.sehn.tech/refs/devops-with-docker/)
|
||||||
|
|
||||||
|
The bare metal route is complicated to setup but makes it easier should
|
||||||
|
you want to contribute some code back. You need to configure and run the
|
||||||
|
above mentioned components yourself.
|
||||||
|
|
||||||
|
### Use the Installation Script {#docker_script}
|
||||||
|
|
||||||
|
Paperless provides an interactive installation script to setup a Docker Compose
|
||||||
|
installation. The script asks for a couple configuration options, and will then create the
|
||||||
|
necessary configuration files, pull the docker image, start Paperless-ngx and create your superuser
|
||||||
|
account. The script essentially automatically performs the steps described in [Docker setup](#docker).
|
||||||
|
|
||||||
|
1. Make sure that Docker and Docker Compose are [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
||||||
|
|
||||||
|
2. Download and run the installation script:
|
||||||
|
|
||||||
If you just want Paperless-ngx running quickly, use our installation script:
|
|
||||||
```shell-session
|
```shell-session
|
||||||
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||||
```
|
```
|
||||||
_If piping into a shell directly from the internet makes you nervous, inspect [the script](https://github.com/paperless-ngx/paperless-ngx/blob/main/install-paperless-ngx.sh) first!_
|
|
||||||
|
|
||||||
## Overview
|
!!! note
|
||||||
|
|
||||||
Choose the installation route that best fits your setup:
|
macOS users will need to install [gnu-sed](https://formulae.brew.sh/formula/gnu-sed) with support
|
||||||
|
for running as `sed` as well as [wget](https://formulae.brew.sh/formula/wget).
|
||||||
|
|
||||||
| Route | Best for | Effort |
|
### Use Docker Compose {#docker}
|
||||||
| ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------ |
|
|
||||||
| [Installation script](#docker_script) | Fastest first-time setup with guided prompts (recommended for most users) | Low |
|
|
||||||
| [Docker Compose templates](#docker) | Manual control over compose files and settings | Medium |
|
|
||||||
| [Bare metal](#bare_metal) | Advanced setups, packaging, and development-adjacent workflows | High |
|
|
||||||
| [Hosted providers (wiki)](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects#hosting-providers) | Managed hosting options maintained by the community — check details carefully | Varies |
|
|
||||||
|
|
||||||
For most users, Docker is the best option. It is faster to set up,
|
1. Make sure that Docker and Docker Compose are [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
||||||
easier to maintain, and ships with sensible defaults.
|
|
||||||
|
|
||||||
The bare-metal route gives you more control, but it requires manual
|
2. Go to the [/docker/compose directory on the project
|
||||||
installation and operation of all components. It is usually best suited
|
|
||||||
for advanced users and contributors.
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
|
|
||||||
Because [superuser](usage.md#superusers) accounts have full access to all objects and documents, you may want to create a separate user account for daily use,
|
|
||||||
or "downgrade" your superuser account to a normal user account after setup.
|
|
||||||
|
|
||||||
## Installation Script {#docker_script}
|
|
||||||
|
|
||||||
Paperless-ngx provides an interactive script for Docker Compose setups.
|
|
||||||
It asks a few configuration questions, then creates the required files,
|
|
||||||
pulls the image, starts the containers, and creates your [superuser](usage.md#superusers)
|
|
||||||
account. In short, it automates the [Docker Compose setup](#docker) described below.
|
|
||||||
|
|
||||||
#### Prerequisites
|
|
||||||
|
|
||||||
- Docker and Docker Compose must be [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
|
||||||
- macOS users will need [GNU sed](https://formulae.brew.sh/formula/gnu-sed) with support for running as `sed` as well as [wget](https://formulae.brew.sh/formula/wget).
|
|
||||||
|
|
||||||
#### Run the installation script
|
|
||||||
|
|
||||||
```shell-session
|
|
||||||
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After installation
|
|
||||||
|
|
||||||
Paperless-ngx should be available at `http://127.0.0.1:8000` (or similar,
|
|
||||||
depending on your configuration) and you will be able to login with the
|
|
||||||
credentials you provided during the installation script.
|
|
||||||
|
|
||||||
## Docker Compose Install {#docker}
|
|
||||||
|
|
||||||
#### Prerequisites
|
|
||||||
|
|
||||||
- Docker and Docker Compose must be [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
|
||||||
|
|
||||||
#### Installation
|
|
||||||
|
|
||||||
1. Go to the [/docker/compose directory on the project
|
|
||||||
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose){:target="\_blank"}
|
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose){:target="\_blank"}
|
||||||
and download one `docker-compose.*.yml` file for your preferred
|
and download one of the `docker-compose.*.yml` files, depending on which database backend
|
||||||
database backend. Save it in a local directory as `docker-compose.yml`.
|
you want to use. Place the files in a local directory and rename it `docker-compose.yml`. Download the
|
||||||
Also download `docker-compose.env` and `.env` into that same directory.
|
`docker-compose.env` file and the `.env` file as well in the same directory.
|
||||||
|
|
||||||
If you want to enable optional support for Office and other documents, download a
|
If you want to enable optional support for Office and other documents, download a
|
||||||
file with `-tika` in the file name.
|
file with `-tika` in the file name.
|
||||||
@@ -81,16 +56,15 @@ credentials you provided during the installation script.
|
|||||||
For new installations, it is recommended to use PostgreSQL as the
|
For new installations, it is recommended to use PostgreSQL as the
|
||||||
database backend.
|
database backend.
|
||||||
|
|
||||||
2. Modify `docker-compose.yml` as needed. For example, you may want to
|
3. Modify `docker-compose.yml` as needed. For example, you may want to change the paths to the
|
||||||
change the paths for `consume`, `media`, and other directories to
|
consumption, media etc. directories to use 'bind mounts'.
|
||||||
use bind mounts.
|
|
||||||
Find the line that specifies where to mount the directory, e.g.:
|
Find the line that specifies where to mount the directory, e.g.:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- ./consume:/usr/src/paperless/consume
|
- ./consume:/usr/src/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the part _before_ the colon with your local directory:
|
Replace the part _before_ the colon with a local directory of your choice:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
||||||
@@ -104,15 +78,38 @@ credentials you provided during the installation script.
|
|||||||
- 8010:8000
|
- 8010:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Modify `docker-compose.env` with any configuration options you need.
|
**Rootless**
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
It is currently not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
|
||||||
|
|
||||||
|
If you want to run Paperless as a rootless container, you will need
|
||||||
|
to do the following in your `docker-compose.yml`:
|
||||||
|
|
||||||
|
- set the `user` running the container to map to the `paperless`
|
||||||
|
user in the container. This value (`user_id` below), should be
|
||||||
|
the same id that `USERMAP_UID` and `USERMAP_GID` are set to in
|
||||||
|
the next step. See `USERMAP_UID` and `USERMAP_GID`
|
||||||
|
[here](configuration.md#docker).
|
||||||
|
|
||||||
|
Your entry for Paperless should contain something like:
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> webserver:
|
||||||
|
> image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
|
> user: <user_id>
|
||||||
|
> ```
|
||||||
|
|
||||||
|
4. Modify `docker-compose.env` with any configuration options you'd like.
|
||||||
See the [configuration documentation](configuration.md) for all options.
|
See the [configuration documentation](configuration.md) for all options.
|
||||||
|
|
||||||
You may also need to set `USERMAP_UID` and `USERMAP_GID` to
|
You may also need to set `USERMAP_UID` and `USERMAP_GID` to
|
||||||
the UID and GID of your user on the host system. Use `id -u` and
|
the uid and gid of your user on the host system. Use `id -u` and
|
||||||
`id -g` to get these values. This ensures both the container and the
|
`id -g` to get these. This ensures that both the container and the host
|
||||||
host user can write to the consumption directory. If your UID and
|
user have write access to the consumption directory. If your UID
|
||||||
GID are `1000` (the default for the first normal user on many
|
and GID on the host system is 1000 (the default for the first normal
|
||||||
systems), this usually works out of the box without
|
user on most systems), it will work out of the box without any
|
||||||
modifications. Run `id "username"` to check.
|
modifications. Run `id "username"` to check.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -121,62 +118,78 @@ credentials you provided during the installation script.
|
|||||||
appending `_FILE` to configuration values. For example [`PAPERLESS_DBUSER`](configuration.md#PAPERLESS_DBUSER)
|
appending `_FILE` to configuration values. For example [`PAPERLESS_DBUSER`](configuration.md#PAPERLESS_DBUSER)
|
||||||
can be set using `PAPERLESS_DBUSER_FILE=/var/run/secrets/password.txt`.
|
can be set using `PAPERLESS_DBUSER_FILE=/var/run/secrets/password.txt`.
|
||||||
|
|
||||||
4. Run `docker compose pull`. This pulls the image from the GitHub container registry
|
!!! warning
|
||||||
by default, but you can pull from Docker Hub by changing the `image`
|
|
||||||
|
Some file systems such as NFS network shares don't support file
|
||||||
|
system notifications with `inotify`. When storing the consumption
|
||||||
|
directory on such a file system, paperless will not pick up new
|
||||||
|
files with the default configuration. You will need to use
|
||||||
|
[`PAPERLESS_CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL), which will disable inotify.
|
||||||
|
|
||||||
|
5. Run `docker compose pull`. This will pull the image from the GitHub container registry
|
||||||
|
by default but you can change the image to pull from Docker Hub by changing the `image`
|
||||||
line to `image: paperlessngx/paperless-ngx:latest`.
|
line to `image: paperlessngx/paperless-ngx:latest`.
|
||||||
|
|
||||||
5. Run `docker compose up -d`. This will create and start the necessary containers.
|
6. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||||
|
|
||||||
#### After installation
|
7. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
||||||
|
(or similar, depending on your configuration). When you first access the web interface, you will be
|
||||||
|
prompted to create a superuser account.
|
||||||
|
|
||||||
Your Paperless-ngx instance should now be accessible at
|
### Build the Docker image yourself {#docker_build}
|
||||||
`http://127.0.0.1:8000` (or similar, depending on your configuration).
|
|
||||||
When you first access the web interface, you will be prompted to create
|
|
||||||
a [superuser](usage.md#superusers) account.
|
|
||||||
|
|
||||||
#### Optional Advanced Compose Configurations {#advanced_compose data-toc-label="Advanced Compose Configurations"}
|
1. Clone the entire repository of paperless:
|
||||||
|
|
||||||
**Rootless**
|
```shell-session
|
||||||
|
git clone https://github.com/paperless-ngx/paperless-ngx
|
||||||
|
```
|
||||||
|
|
||||||
!!! warning
|
The main branch always reflects the latest stable version.
|
||||||
|
|
||||||
It is currently not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
|
2. Copy one of the `docker/compose/docker-compose.*.yml` to
|
||||||
|
`docker-compose.yml` in the root folder, depending on which database
|
||||||
|
backend you want to use. Copy `docker-compose.env` into the project
|
||||||
|
root as well.
|
||||||
|
|
||||||
If you want to run Paperless as a rootless container, make this
|
3. In the `docker-compose.yml` file, find the line that instructs
|
||||||
change in `docker-compose.yml`:
|
Docker Compose to pull the paperless image from Docker Hub:
|
||||||
|
|
||||||
- Set the `user` running the container to map to the `paperless`
|
```yaml
|
||||||
user in the container. This value (`user_id` below) should be
|
webserver:
|
||||||
the same ID that `USERMAP_UID` and `USERMAP_GID` are set to in
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
`docker-compose.env`. See `USERMAP_UID` and `USERMAP_GID`
|
```
|
||||||
[here](configuration.md#docker).
|
|
||||||
|
|
||||||
Your entry for Paperless should contain something like:
|
and replace it with a line that instructs Docker Compose to build
|
||||||
|
the image from the current working directory instead:
|
||||||
|
|
||||||
> ```
|
```yaml
|
||||||
> webserver:
|
webserver:
|
||||||
> image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
build:
|
||||||
> user: <user_id>
|
context: .
|
||||||
> ```
|
```
|
||||||
|
|
||||||
**File systems without inotify support (e.g. NFS)**
|
4. Follow the [Docker setup](#docker) above except when asked to run
|
||||||
|
`docker compose pull` to pull the image, run
|
||||||
|
|
||||||
Some file systems, such as NFS network shares, don't support file system
|
```shell-session
|
||||||
notifications with `inotify`. When the consumption directory is on such a
|
docker compose build
|
||||||
file system, Paperless-ngx will not pick up new files with the default
|
```
|
||||||
configuration. Use [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING)
|
|
||||||
to enable polling and disable inotify. See [here](configuration.md#polling).
|
|
||||||
|
|
||||||
## Bare Metal Install {#bare_metal}
|
instead to build the image.
|
||||||
|
|
||||||
#### Prerequisites
|
### Bare Metal Route {#bare_metal}
|
||||||
|
|
||||||
- Paperless runs on Linux only, Windows is not supported.
|
Paperless runs on linux only. The following procedure has been tested on
|
||||||
- Python 3.11, 3.12, 3.13, or 3.14 is required. As a policy, Paperless-ngx aims to support at least the three most recent Python versions and drops support for versions as they reach end-of-life. Newer versions may work, but some dependencies may not be fully compatible.
|
a minimal installation of Debian/Buster, which is the current stable
|
||||||
|
release at the time of writing. Windows is not and will never be
|
||||||
|
supported.
|
||||||
|
|
||||||
#### Installation
|
Paperless requires Python 3. At this time, 3.10 - 3.12 are tested versions.
|
||||||
|
Newer versions may work, but some dependencies may not fully support newer versions.
|
||||||
|
Support for older Python versions may be dropped as they reach end of life or as newer versions
|
||||||
|
are released, dependency support is confirmed, etc.
|
||||||
|
|
||||||
1. Install dependencies. Paperless requires the following packages:
|
1. Install dependencies. Paperless requires the following packages.
|
||||||
|
|
||||||
- `python3`
|
- `python3`
|
||||||
- `python3-pip`
|
- `python3-pip`
|
||||||
@@ -190,12 +203,13 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
- `libpq-dev` for PostgreSQL
|
- `libpq-dev` for PostgreSQL
|
||||||
- `libmagic-dev` for mime type detection
|
- `libmagic-dev` for mime type detection
|
||||||
- `mariadb-client` for MariaDB compile time
|
- `mariadb-client` for MariaDB compile time
|
||||||
|
- `libzbar0` for barcode detection
|
||||||
- `poppler-utils` for barcode detection
|
- `poppler-utils` for barcode detection
|
||||||
|
|
||||||
Use this list for your preferred package management:
|
Use this list for your preferred package management:
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev poppler-utils
|
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev libzbar0 poppler-utils
|
||||||
```
|
```
|
||||||
|
|
||||||
These dependencies are required for OCRmyPDF, which is used for text
|
These dependencies are required for OCRmyPDF, which is used for text
|
||||||
@@ -239,8 +253,8 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
|
|
||||||
2. Install `redis` >= 6.0 and configure it to start automatically.
|
2. Install `redis` >= 6.0 and configure it to start automatically.
|
||||||
|
|
||||||
3. Optional: Install `postgresql` and configure a database, user, and
|
3. Optional. Install `postgresql` and configure a database, user and
|
||||||
password for Paperless-ngx. If you do not wish to use PostgreSQL,
|
password for paperless. If you do not wish to use PostgreSQL,
|
||||||
MariaDB and SQLite are available as well.
|
MariaDB and SQLite are available as well.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -249,60 +263,61 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
extension](https://code.djangoproject.com/wiki/JSON1Extension) is
|
extension](https://code.djangoproject.com/wiki/JSON1Extension) is
|
||||||
enabled. This is usually the case, but not always.
|
enabled. This is usually the case, but not always.
|
||||||
|
|
||||||
4. Create a system user with a new home folder in which you want
|
4. Create a system user with a new home folder under which you wish
|
||||||
to run Paperless-ngx.
|
to run paperless.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
adduser paperless --system --home /opt/paperless --group
|
adduser paperless --system --home /opt/paperless --group
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Download a release archive from
|
5. Get the release archive from
|
||||||
<https://github.com/paperless-ngx/paperless-ngx/releases>. For example:
|
<https://github.com/paperless-ngx/paperless-ngx/releases> for example with
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
curl -O -L https://github.com/paperless-ngx/paperless-ngx/releases/download/vX.Y.Z/paperless-ngx-vX.Y.Z.tar.xz
|
curl -O -L https://github.com/paperless-ngx/paperless-ngx/releases/download/v1.10.2/paperless-ngx-v1.10.2.tar.xz
|
||||||
```
|
```
|
||||||
|
|
||||||
Extract the archive with
|
Extract the archive with
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
tar -xf paperless-ngx-vX.Y.Z.tar.xz
|
tar -xf paperless-ngx-v1.10.2.tar.xz
|
||||||
```
|
```
|
||||||
|
|
||||||
and copy the contents to the home directory of the user you created
|
and copy the contents to the
|
||||||
earlier (`/opt/paperless`).
|
home folder of the user you created before (`/opt/paperless`).
|
||||||
|
|
||||||
Optional: If you cloned the Git repository, you will need to
|
Optional: If you cloned the git repo, you will have to
|
||||||
compile the frontend yourself. See [here](development.md#front-end-development)
|
compile the frontend yourself, see [here](development.md#front-end-development)
|
||||||
and use the `build` step, not `serve`.
|
and use the `build` step, not `serve`.
|
||||||
|
|
||||||
6. Configure Paperless-ngx. See [configuration](configuration.md) for details.
|
6. Configure paperless. See [configuration](configuration.md) for details.
|
||||||
Edit the included `paperless.conf` and adjust the settings to your
|
Edit the included `paperless.conf` and adjust the settings to your
|
||||||
needs. Required settings for getting Paperless-ngx running are:
|
needs. Required settings for getting
|
||||||
|
paperless running are:
|
||||||
|
|
||||||
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your Redis server, such as
|
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your redis server, such as
|
||||||
`redis://localhost:6379`.
|
<redis://localhost:6379>.
|
||||||
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) is optional, and should be one of `postgres`,
|
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) optional, and should be one of `postgres`,
|
||||||
`mariadb`, or `sqlite`
|
`mariadb`, or `sqlite`
|
||||||
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
|
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
|
||||||
PostgreSQL server is running. Do not configure this to use
|
PostgreSQL server is running. Do not configure this to use
|
||||||
SQLite instead. Also configure port, database name, user and
|
SQLite instead. Also configure port, database name, user and
|
||||||
password as necessary.
|
password as necessary.
|
||||||
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to the folder
|
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to a folder which
|
||||||
that Paperless-ngx should watch for incoming documents.
|
paperless should watch for documents. You might want to have
|
||||||
Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
|
this somewhere else. Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
|
||||||
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where Paperless-ngx stores its data.
|
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where paperless stores its data.
|
||||||
If needed, these can point to the same directory.
|
If you like, you can point both to the same directory.
|
||||||
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
|
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
|
||||||
characters. It's used for authentication. Failure to do so
|
characters. It's used for authentication. Failure to do so
|
||||||
allows third parties to forge authentication credentials.
|
allows third parties to forge authentication credentials.
|
||||||
- Set [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
|
- [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
|
||||||
point to your domain. Please see
|
point to your domain. Please see
|
||||||
[configuration](configuration.md) for more
|
[configuration](configuration.md) for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
You can make many more adjustments, especially for OCR.
|
Many more adjustments can be made to paperless, especially the OCR
|
||||||
The following options are recommended for most users:
|
part. The following options are recommended for everyone:
|
||||||
|
|
||||||
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
|
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
|
||||||
documents are written in.
|
documents are written in.
|
||||||
@@ -312,14 +327,15 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
|
|
||||||
Ensure your Redis instance [is secured](https://redis.io/docs/latest/operate/oss_and_stack/management/security/).
|
Ensure your Redis instance [is secured](https://redis.io/docs/latest/operate/oss_and_stack/management/security/).
|
||||||
|
|
||||||
7. Create the following directories if they do not already exist:
|
7. Create the following directories if they are missing:
|
||||||
|
|
||||||
- `/opt/paperless/media`
|
- `/opt/paperless/media`
|
||||||
- `/opt/paperless/data`
|
- `/opt/paperless/data`
|
||||||
- `/opt/paperless/consume`
|
- `/opt/paperless/consume`
|
||||||
|
|
||||||
Adjust these paths if you configured different folders.
|
Adjust as necessary if you configured different folders.
|
||||||
Then verify that the `paperless` user has write permissions:
|
Ensure that the paperless user has write permissions for every one
|
||||||
|
of these folders with
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
ls -l -d /opt/paperless/media
|
ls -l -d /opt/paperless/media
|
||||||
@@ -333,44 +349,45 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
sudo chown paperless:paperless /opt/paperless/consume
|
sudo chown paperless:paperless /opt/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Install Python dependencies from `requirements.txt`.
|
8. Install python requirements from the `requirements.txt` file.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
sudo -Hu paperless pip3 install -r requirements.txt
|
sudo -Hu paperless pip3 install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
This will install all Python dependencies in the home directory of
|
This will install all python dependencies in the home directory of
|
||||||
the new paperless user.
|
the new paperless user.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
You can use a virtual environment if you prefer. If you do,
|
It is up to you if you wish to use a virtual environment or not for the Python
|
||||||
you may need to adjust the example scripts for your virtual
|
dependencies. This is an alternative to the above and may require adjusting
|
||||||
environment paths.
|
the example scripts to utilize the virtual environment paths
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
If you use modern Python tooling, such as `uv`, installation will not include
|
If you use modern Python tooling, such as `uv`, installation will not include
|
||||||
dependencies for PostgreSQL or MariaDB. You can select those
|
dependencies for Postgres or Mariadb. You can select those extras with `--extra <EXTRA>`
|
||||||
extras with `--extra <EXTRA>`, or install all extras with
|
or all with `--all-extras`
|
||||||
`--all-extras`.
|
|
||||||
|
|
||||||
9. Go to `/opt/paperless/src` and execute the following command:
|
9. Go to `/opt/paperless/src`, and execute the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# This creates the database schema.
|
# This creates the database schema.
|
||||||
sudo -Hu paperless python3 manage.py migrate
|
sudo -Hu paperless python3 manage.py migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
10. Optional: Test that Paperless-ngx is working by running
|
When you first access the web interface you will be prompted to create a superuser account.
|
||||||
|
|
||||||
|
10. Optional: Test that paperless is working by executing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Manually starts the webserver
|
# Manually starts the webserver
|
||||||
sudo -Hu paperless python3 manage.py runserver
|
sudo -Hu paperless python3 manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
Then point your browser to `http://localhost:8000` if
|
and pointing your browser to http://localhost:8000 if
|
||||||
accessing from the same device on which Paperless-ngx is installed.
|
accessing from the same devices on which paperless is installed.
|
||||||
If accessing from another machine, set up systemd services. You may need
|
If accessing from another machine, set up systemd services. You may need
|
||||||
to set `PAPERLESS_DEBUG=true` in order for the development server to work
|
to set `PAPERLESS_DEBUG=true` in order for the development server to work
|
||||||
normally in your browser.
|
normally in your browser.
|
||||||
@@ -378,24 +395,23 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
This is a development server which should not be used in production.
|
This is a development server which should not be used in production.
|
||||||
It is not audited for security, and performance is inferior to
|
It is not audited for security and performance is inferior to
|
||||||
production-ready web servers.
|
production ready web servers.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
This will not start the consumer. Paperless does this in a separate
|
This will not start the consumer. Paperless does this in a separate
|
||||||
process.
|
process.
|
||||||
|
|
||||||
11. Set up systemd services to run Paperless-ngx automatically. You may use
|
11. Setup systemd services to run paperless automatically. You may use
|
||||||
the service definition files included in the `scripts` folder as a
|
the service definition files included in the `scripts` folder as a
|
||||||
starting point.
|
starting point.
|
||||||
|
|
||||||
Paperless needs:
|
Paperless needs the `webserver` script to run the webserver, the
|
||||||
|
`consumer` script to watch the input folder, `taskqueue` for the
|
||||||
- The `webserver` script to run the webserver.
|
background workers used to handle things like document consumption
|
||||||
- The `consumer` script to watch the input folder.
|
and the `scheduler` script to run tasks such as email checking at
|
||||||
- The `taskqueue` script for background workers (document consumption, etc.).
|
certain times .
|
||||||
- The `scheduler` script for periodic tasks such as email checking.
|
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -404,9 +420,9 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
`Require=paperless-webserver.socket` in the `webserver` script
|
`Require=paperless-webserver.socket` in the `webserver` script
|
||||||
and configure `granian` to listen on port 80 (set `GRANIAN_PORT`).
|
and configure `granian` to listen on port 80 (set `GRANIAN_PORT`).
|
||||||
|
|
||||||
These services rely on Redis and optionally the database server, but
|
These services rely on redis and optionally the database server, but
|
||||||
don't need to be started in any particular order. The example files
|
don't need to be started in any particular order. The example files
|
||||||
depend on Redis being started. If you use a database server, you
|
depend on redis being started. If you use a database server, you
|
||||||
should add additional dependencies.
|
should add additional dependencies.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -416,15 +432,18 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
If Celery won't start, check
|
If celery won't start (check with
|
||||||
`sudo systemctl status paperless-task-queue.service` for
|
`sudo systemctl status paperless-task-queue.service` for
|
||||||
`paperless-task-queue.service` and `paperless-scheduler.service`.
|
paperless-task-queue.service and paperless-scheduler.service
|
||||||
You may need to change the path in the files. Example:
|
) you need to change the path in the files. Example:
|
||||||
`ExecStart=/opt/paperless/.local/bin/celery --app paperless worker --loglevel INFO`
|
`ExecStart=/opt/paperless/.local/bin/celery --app paperless worker --loglevel INFO`
|
||||||
|
|
||||||
12. Configure ImageMagick to allow processing of PDF documents. Most
|
12. Optional: Install a samba server and make the consumption folder
|
||||||
|
available as a network share.
|
||||||
|
|
||||||
|
13. Configure ImageMagick to allow processing of PDF documents. Most
|
||||||
distributions have this disabled by default, since PDF documents can
|
distributions have this disabled by default, since PDF documents can
|
||||||
contain malware. If you don't do this, Paperless-ngx will fall back to
|
contain malware. If you don't do this, paperless will fall back to
|
||||||
Ghostscript for certain steps such as thumbnail generation.
|
Ghostscript for certain steps such as thumbnail generation.
|
||||||
|
|
||||||
Edit `/etc/ImageMagick-6/policy.xml` and adjust
|
Edit `/etc/ImageMagick-6/policy.xml` and adjust
|
||||||
@@ -439,38 +458,32 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
<policy domain="coder" rights="read|write" pattern="PDF" />
|
<policy domain="coder" rights="read|write" pattern="PDF" />
|
||||||
```
|
```
|
||||||
|
|
||||||
**Optional: Install the [jbig2enc](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html) encoder.**
|
14. Optional: Install the
|
||||||
This will reduce the size of generated PDF documents. You'll most likely need to compile this yourself, because this
|
[jbig2enc](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html)
|
||||||
software has been patented until around 2017 and binary packages are not available for most distributions.
|
encoder. This will reduce the size of generated PDF documents.
|
||||||
|
You'll most likely need to compile this by yourself, because this
|
||||||
|
software has been patented until around 2017 and binary packages are
|
||||||
|
not available for most distributions.
|
||||||
|
|
||||||
**Optional: download the NLTK data**
|
15. Optional: If using the NLTK machine learning processing (see
|
||||||
If using the NLTK machine-learning processing (see [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) for details),
|
[`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) for details),
|
||||||
download the NLTK data for the Snowball Stemmer, Stopwords and Punkt tokenizer to `/usr/share/nltk_data`. Refer to the [NLTK
|
download the NLTK data for the Snowball
|
||||||
instructions](https://www.nltk.org/data.html) for details on how to download the data.
|
Stemmer, Stopwords and Punkt tokenizer to `/usr/share/nltk_data`. Refer to the [NLTK
|
||||||
|
instructions](https://www.nltk.org/data.html) for details on how to
|
||||||
|
download the data.
|
||||||
|
|
||||||
#### After installation
|
# Migrating to Paperless-ngx
|
||||||
|
|
||||||
Your Paperless-ngx instance should now be accessible at `http://localhost:8000` (or similar, depending on your configuration).
|
Migration is possible both from Paperless-ng or directly from the
|
||||||
When you first access the web interface you will be prompted to create a [superuser](usage.md#superusers) account.
|
'original' Paperless.
|
||||||
|
|
||||||
## Build the Docker image yourself {#docker_build data-toc-label="Building the Docker image"}
|
## Migrating from Paperless-ng
|
||||||
|
|
||||||
Building the Docker image yourself is typically used for development, but it can also be used for production
|
Paperless-ngx is meant to be a drop-in replacement for Paperless-ng and
|
||||||
if you want to customize the image. See [Building the Docker image](development.md#docker_build) in the
|
thus upgrading should be trivial for most users, especially when using
|
||||||
development documentation.
|
docker. However, as with any major change, it is recommended to take a
|
||||||
|
|
||||||
## Migrating to Paperless-ngx
|
|
||||||
|
|
||||||
You can migrate to Paperless-ngx from Paperless-ng or from the original
|
|
||||||
Paperless project.
|
|
||||||
|
|
||||||
<h3 id="migration_ng">Migrating from Paperless-ng</h3>
|
|
||||||
|
|
||||||
Paperless-ngx is meant to be a drop-in replacement for Paperless-ng, and
|
|
||||||
upgrading should be trivial for most users, especially when using
|
|
||||||
Docker. However, as with any major change, it is recommended to take a
|
|
||||||
full backup first. Once you are ready, simply change the docker image to
|
full backup first. Once you are ready, simply change the docker image to
|
||||||
point to the new source. For example, if using Docker Compose, edit
|
point to the new source. E.g. if using Docker Compose, edit
|
||||||
`docker-compose.yml` and change:
|
`docker-compose.yml` and change:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -483,64 +496,66 @@ to
|
|||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
and then run `docker compose up -d`, which will pull the new image and
|
and then run `docker compose up -d` which will pull the new image
|
||||||
recreate the container. That's it.
|
recreate the container. That's it!
|
||||||
|
|
||||||
Users who installed with the bare-metal route should also update their
|
Users who installed with the bare-metal route should also update their
|
||||||
Git clone to point to `https://github.com/paperless-ngx/paperless-ngx`,
|
Git clone to point to `https://github.com/paperless-ngx/paperless-ngx`,
|
||||||
for example using:
|
e.g. using the command
|
||||||
`git remote set-url origin https://github.com/paperless-ngx/paperless-ngx`
|
`git remote set-url origin https://github.com/paperless-ngx/paperless-ngx`
|
||||||
and then pull the latest version.
|
and then pull the latest version.
|
||||||
|
|
||||||
<h3 id="migration_paperless">Migrating from Paperless</h3>
|
## Migrating from Paperless
|
||||||
|
|
||||||
At its core, Paperless-ngx is still Paperless and fully compatible.
|
At its core, paperless-ngx is still paperless and fully compatible.
|
||||||
However, some things have changed under the hood, so you need to adapt
|
However, some things have changed under the hood, so you need to adapt
|
||||||
your setup depending on how you installed Paperless.
|
your setup depending on how you installed paperless.
|
||||||
|
|
||||||
This section describes how to update an existing Paperless Docker
|
This setup describes how to update an existing paperless Docker
|
||||||
installation. Keep these points in mind:
|
installation. The important things to keep in mind are as follows:
|
||||||
|
|
||||||
- Read the [changelog](changelog.md) and
|
- Read the [changelog](changelog.md) and
|
||||||
take note of breaking changes.
|
take note of breaking changes.
|
||||||
- Decide whether to stay on SQLite or migrate to PostgreSQL.
|
- You should decide if you want to stick with SQLite or want to
|
||||||
Both work fine with Paperless-ngx.
|
migrate your database to PostgreSQL. See [documentation](#sqlite_to_psql)
|
||||||
However, if you already have a database server running
|
for details on
|
||||||
for other services, you might as well use it for Paperless as well.
|
how to move your data from SQLite to PostgreSQL. Both work fine with
|
||||||
- The task scheduler of Paperless, which is used to execute periodic
|
paperless. However, if you already have a database server running
|
||||||
|
for other services, you might as well use it for paperless as well.
|
||||||
|
- The task scheduler of paperless, which is used to execute periodic
|
||||||
tasks such as email checking and maintenance, requires a
|
tasks such as email checking and maintenance, requires a
|
||||||
[Redis](https://redis.io/) message broker instance. The
|
[redis](https://redis.io/) message broker instance. The
|
||||||
Docker Compose route takes care of that.
|
Docker Compose route takes care of that.
|
||||||
- The layout of the folder structure for your documents and data
|
- The layout of the folder structure for your documents and data
|
||||||
remains the same, so you can plug your old Docker volumes into
|
remains the same, so you can just plug your old docker volumes into
|
||||||
paperless-ngx and expect it to find everything where it should be.
|
paperless-ngx and expect it to find everything where it should be.
|
||||||
|
|
||||||
Migration to Paperless-ngx is then performed in a few simple steps:
|
Migration to paperless-ngx is then performed in a few simple steps:
|
||||||
|
|
||||||
1. Stop Paperless.
|
1. Stop paperless.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/current/paperless
|
cd /path/to/current/paperless
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create a backup for two reasons: if something goes wrong, you still
|
2. Do a backup for two purposes: If something goes wrong, you still
|
||||||
have your data; and if you don't like paperless-ngx, you can
|
have your data. Second, if you don't like paperless-ngx, you can
|
||||||
switch back to Paperless.
|
switch back to paperless.
|
||||||
|
|
||||||
3. Download the latest release of Paperless-ngx. You can either use
|
3. Download the latest release of paperless-ngx. You can either go with
|
||||||
the Docker Compose files from
|
the Docker Compose files from
|
||||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||||
or clone the repository to build the image yourself (see
|
or clone the repository to build the image yourself (see
|
||||||
[development docs](development.md#docker_build)). You can either replace your current paperless
|
[above](#docker_build)). You can
|
||||||
folder or put Paperless-ngx in
|
either replace your current paperless folder or put paperless-ngx in
|
||||||
a different location.
|
a different location.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
Paperless-ngx includes a `.env` file. This will set the project name
|
Paperless-ngx includes a `.env` file. This will set the project name
|
||||||
for Docker Compose to `paperless`, which will also define the
|
for docker compose to `paperless`, which will also define the name
|
||||||
volume names created by Paperless-ngx. However, if you notice that
|
of the volumes by paperless-ngx. However, if you experience that
|
||||||
paperless-ngx is not using your old paperless volumes, verify the
|
paperless-ngx is not using your old paperless volumes, verify the
|
||||||
names of your volumes with
|
names of your volumes with
|
||||||
|
|
||||||
@@ -556,10 +571,10 @@ Migration to Paperless-ngx is then performed in a few simple steps:
|
|||||||
after you migrated your existing SQLite database.
|
after you migrated your existing SQLite database.
|
||||||
|
|
||||||
5. Adjust `docker-compose.yml` and `docker-compose.env` to your needs.
|
5. Adjust `docker-compose.yml` and `docker-compose.env` to your needs.
|
||||||
See [Docker setup](#docker) for details on
|
See [Docker setup](#docker) details on
|
||||||
which edits are recommended.
|
which edits are advised.
|
||||||
|
|
||||||
6. Follow the update procedure in [Update paperless](administration.md#updating).
|
6. [Update paperless.](administration.md#updating)
|
||||||
|
|
||||||
7. In order to find your existing documents with the new search
|
7. In order to find your existing documents with the new search
|
||||||
feature, you need to invoke a one-time operation that will create
|
feature, you need to invoke a one-time operation that will create
|
||||||
@@ -570,99 +585,136 @@ Migration to Paperless-ngx is then performed in a few simple steps:
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will migrate your database and create the search index. After
|
This will migrate your database and create the search index. After
|
||||||
that, Paperless-ngx will maintain the index automatically.
|
that, paperless will take care of maintaining the index by itself.
|
||||||
|
|
||||||
8. Start Paperless-ngx.
|
8. Start paperless-ngx.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This will run Paperless-ngx in the background and automatically start it
|
This will run paperless in the background and automatically start it
|
||||||
on system boot.
|
on system boot.
|
||||||
|
|
||||||
9. Paperless may have installed a permanent redirect to `admin/` in your
|
9. Paperless installed a permanent redirect to `admin/` in your
|
||||||
browser. This redirect is still in place and prevents access to the
|
browser. This redirect is still in place and prevents access to the
|
||||||
new UI. Clear your browser cache to fix this.
|
new UI. Clear your browsing cache in order to fix this.
|
||||||
|
|
||||||
10. Optionally, follow the instructions below to migrate your existing
|
10. Optionally, follow the instructions below to migrate your existing
|
||||||
data to PostgreSQL.
|
data to PostgreSQL.
|
||||||
|
|
||||||
<h3 id="migration_lsio">Migrating from LinuxServer.io Docker Image</h3>
|
## Migrating from LinuxServer.io Docker Image
|
||||||
|
|
||||||
As with any upgrade or large change, it is highly recommended to
|
As with any upgrades and large changes, it is highly recommended to
|
||||||
create a backup before starting. This assumes the image was running
|
create a backup before starting. This assumes the image was running
|
||||||
using Docker Compose, but the instructions are translatable to Docker
|
using Docker Compose, but the instructions are translatable to Docker
|
||||||
commands as well.
|
commands as well.
|
||||||
|
|
||||||
1. Stop and remove the Paperless container.
|
1. Stop and remove the paperless container
|
||||||
2. If using an external database, stop that container.
|
2. If using an external database, stop the container
|
||||||
3. Update Redis configuration.
|
3. Update Redis configuration
|
||||||
|
|
||||||
1. If `REDIS_URL` is already set, change it to [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS)
|
1. If `REDIS_URL` is already set, change it to [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS)
|
||||||
and continue to step 4.
|
and continue to step 4.
|
||||||
|
|
||||||
1. Otherwise, add a new Redis service in `docker-compose.yml`,
|
1. Otherwise, in the `docker-compose.yml` add a new service for
|
||||||
following [the example compose
|
Redis, following [the example compose
|
||||||
files](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
files](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) so it points to
|
1. Set the environment variable [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) so it points to
|
||||||
the new Redis container.
|
the new Redis container
|
||||||
|
|
||||||
4. Update user mapping.
|
4. Update user mapping
|
||||||
|
|
||||||
1. If set, change the environment variable `PUID` to `USERMAP_UID`.
|
1. If set, change the environment variable `PUID` to `USERMAP_UID`
|
||||||
|
|
||||||
1. If set, change the environment variable `PGID` to `USERMAP_GID`.
|
1. If set, change the environment variable `PGID` to `USERMAP_GID`
|
||||||
|
|
||||||
5. Update configuration paths.
|
5. Update configuration paths
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) to `/config`.
|
1. Set the environment variable [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) to `/config`
|
||||||
|
|
||||||
6. Update media paths.
|
6. Update media paths
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) to
|
1. Set the environment variable [`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) to
|
||||||
`/data/media`.
|
`/data/media`
|
||||||
|
|
||||||
7. Update timezone.
|
7. Update timezone
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to the same
|
1. Set the environment variable [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to the same
|
||||||
value as `TZ`.
|
value as `TZ`
|
||||||
|
|
||||||
8. Modify `image:` to point to
|
8. Modify the `image:` to point to
|
||||||
`ghcr.io/paperless-ngx/paperless-ngx:latest` or a specific version
|
`ghcr.io/paperless-ngx/paperless-ngx:latest` or a specific version
|
||||||
if preferred.
|
if preferred.
|
||||||
9. Start the containers as before, using `docker compose`.
|
9. Start the containers as before, using `docker compose`.
|
||||||
|
|
||||||
## Running Paperless-ngx on less powerful devices {#less-powerful-devices data-toc-label="Less Powerful Devices"}
|
## Moving data from SQLite to PostgreSQL or MySQL/MariaDB {#sqlite_to_psql}
|
||||||
|
|
||||||
Paperless runs on Raspberry Pi. Some tasks can be slow on lower-powered
|
The best way to migrate between database types is to perform an [export](administration.md#exporter) and then
|
||||||
hardware, but a few settings can improve performance:
|
[import](administration.md#importer) into a clean installation of Paperless-ngx.
|
||||||
|
|
||||||
|
## Moving back to Paperless
|
||||||
|
|
||||||
|
Lets say you migrated to Paperless-ngx and used it for a while, but
|
||||||
|
decided that you don't like it and want to move back (If you do, send
|
||||||
|
me a mail about what part you didn't like!), you can totally do that
|
||||||
|
with a few simple steps.
|
||||||
|
|
||||||
|
Paperless-ngx modified the database schema slightly, however, these
|
||||||
|
changes can be reverted while keeping your current data, so that your
|
||||||
|
current data will be compatible with original Paperless. Thumbnails
|
||||||
|
were also changed from PNG to WEBP format and will need to be
|
||||||
|
re-generated.
|
||||||
|
|
||||||
|
Execute this:
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ cd /path/to/paperless
|
||||||
|
$ docker compose run --rm webserver migrate documents 0023
|
||||||
|
```
|
||||||
|
|
||||||
|
Or without docker:
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ cd /path/to/paperless/src
|
||||||
|
$ python3 manage.py migrate documents 0023
|
||||||
|
```
|
||||||
|
|
||||||
|
After regenerating thumbnails, you'll need to clear your cookies
|
||||||
|
(Paperless-ngx comes with updated dependencies that do cookie-processing
|
||||||
|
differently) and probably your cache as well.
|
||||||
|
|
||||||
|
# Considerations for less powerful devices {#less-powerful-devices}
|
||||||
|
|
||||||
|
Paperless runs on Raspberry Pi. However, some things are rather slow on
|
||||||
|
the Pi and configuring some options in paperless can help improve
|
||||||
|
performance immensely:
|
||||||
|
|
||||||
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
|
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
|
||||||
if you encounter issues with SQLite locking.
|
if you encounter issues with SQLite locking.
|
||||||
- If you do not need the filesystem-based consumer, consider disabling it
|
- If you do not need the filesystem-based consumer, consider disabling it
|
||||||
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
|
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
|
||||||
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that Paperless
|
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
|
||||||
OCRs only the first page of your documents. In most cases, this page
|
only OCR the first page of your documents. In most cases, this page
|
||||||
contains enough information to be able to find it.
|
contains enough information to be able to find it.
|
||||||
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
|
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
|
||||||
configured to use all cores. The Raspberry Pi models 3 and up have 4
|
configured to use all cores. The Raspberry Pi models 3 and up have 4
|
||||||
cores, meaning that Paperless will use 2 workers and 2 threads per
|
cores, meaning that paperless will use 2 workers and 2 threads per
|
||||||
worker. This may result in sluggish response times during
|
worker. This may result in sluggish response times during
|
||||||
consumption, so you might want to lower these settings (example: 2
|
consumption, so you might want to lower these settings (example: 2
|
||||||
workers and 1 thread to always have some computing power left for
|
workers and 1 thread to always have some computing power left for
|
||||||
other tasks).
|
other tasks).
|
||||||
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
|
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
|
||||||
OCRing your documents before feeding them into Paperless. Some
|
OCR'ing your documents before feeding them into paperless. Some
|
||||||
scanners are able to do this!
|
scanners are able to do this!
|
||||||
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
|
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
|
||||||
file generation for already OCRed documents, or `always` to skip it
|
file generation for already ocr'ed documents, or `always` to skip it
|
||||||
for all documents.
|
for all documents.
|
||||||
- If you want to perform OCR on the device, consider using
|
- If you want to perform OCR on the device, consider using
|
||||||
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
||||||
less memory at the expense of slightly worse OCR results.
|
less memory at the expense of slightly worse OCR results.
|
||||||
- If using Docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
|
- If using docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
|
||||||
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
|
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
|
||||||
more advanced language processing, which can take more memory and
|
more advanced language processing, which can take more memory and
|
||||||
processing time.
|
processing time.
|
||||||
@@ -674,19 +726,17 @@ For details, refer to [configuration](configuration.md).
|
|||||||
Updating the
|
Updating the
|
||||||
[automatic matching algorithm](advanced_usage.md#automatic-matching) takes quite a bit of time. However, the update mechanism
|
[automatic matching algorithm](advanced_usage.md#automatic-matching) takes quite a bit of time. However, the update mechanism
|
||||||
checks if your data has changed before doing the heavy lifting. If you
|
checks if your data has changed before doing the heavy lifting. If you
|
||||||
experience the algorithm taking too much CPU time, consider changing the
|
experience the algorithm taking too much cpu time, consider changing the
|
||||||
schedule in the admin interface to daily. You can also manually invoke
|
schedule in the admin interface to daily. You can also manually invoke
|
||||||
the task by changing the date and time of the next run to today/now.
|
the task by changing the date and time of the next run to today/now.
|
||||||
|
|
||||||
The actual matching of the algorithm is fast and works on Raspberry Pi
|
The actual matching of the algorithm is fast and works on Raspberry Pi
|
||||||
as well as on any other device.
|
as well as on any other device.
|
||||||
|
|
||||||
## Additional considerations
|
# Using nginx as a reverse proxy {#nginx}
|
||||||
|
|
||||||
**Using a reverse proxy with Paperless-ngx**
|
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx) for user-maintained documentation of using nginx with Paperless-ngx.
|
||||||
|
|
||||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx) for user-maintained documentation on using nginx with Paperless-ngx.
|
# Enhancing security {#security}
|
||||||
|
|
||||||
**Enhancing security**
|
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-Security-Tools-with-Paperless-ngx) for user-maintained documentation of how to configure security tools like Fail2ban with Paperless-ngx.
|
||||||
|
|
||||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-Security-Tools-with-Paperless-ngx) for user-maintained documentation on configuring security tools like Fail2ban with Paperless-ngx.
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
---
|
# Usage Overview
|
||||||
title: Basic Usage
|
|
||||||
---
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
Paperless-ngx is an application that manages your personal documents. With
|
Paperless-ngx is an application that manages your personal documents. With
|
||||||
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
||||||
@@ -89,17 +85,6 @@ You can view the document, edit its metadata, assign tags, correspondents,
|
|||||||
document types, and custom fields. You can also view the document history,
|
document types, and custom fields. You can also view the document history,
|
||||||
download the document or share it via a share link.
|
download the document or share it via a share link.
|
||||||
|
|
||||||
### Document File Versions
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Management Lists
|
### Management Lists
|
||||||
|
|
||||||
Paperless-ngx includes management lists for tags, correspondents, document types
|
Paperless-ngx includes management lists for tags, correspondents, document types
|
||||||
@@ -383,11 +368,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.
|
Superusers can access all parts of the front and backend application as well as any and all objects. Superuser status can only be granted by another superuser.
|
||||||
|
|
||||||
!!! tip
|
|
||||||
|
|
||||||
Because superuser accounts can see all objects and documents, you may want to use a regular account for day-to-day use. Additional superuser accounts can
|
|
||||||
be created via [cli](administration.md#create-superuser) or granted superuser status from an existing superuser account.
|
|
||||||
|
|
||||||
#### Admin Status
|
#### Admin Status
|
||||||
|
|
||||||
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
|
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
|
||||||
@@ -580,22 +560,10 @@ For security reasons, webhooks can be limited to specific ports and disallowed f
|
|||||||
[configuration settings](configuration.md#workflow-webhooks) to change this behavior. If you are allowing non-admins to create workflows,
|
[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.
|
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
|
#### Workflow placeholders
|
||||||
|
|
||||||
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
Titles can be assigned by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
||||||
This allows for complex logic to be used, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
|
This allows for complex logic to be used to generate the title, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
|
||||||
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11).
|
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11).
|
||||||
The template is provided as a string.
|
The template is provided as a string.
|
||||||
|
|
||||||
@@ -617,8 +585,8 @@ applied. You can use the following placeholders in the template with any trigger
|
|||||||
- `{{added_day}}`: added day
|
- `{{added_day}}`: added day
|
||||||
- `{{added_time}}`: added time in HH:MM format
|
- `{{added_time}}`: added time in HH:MM format
|
||||||
- `{{original_filename}}`: original file name without extension
|
- `{{original_filename}}`: original file name without extension
|
||||||
- `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`)
|
- `{{filename}}`: current file name without extension
|
||||||
- `{{doc_title}}`: current document title (cannot be used in title assignment)
|
- `{{doc_title}}`: current document title
|
||||||
|
|
||||||
The following placeholders are only available for "added" or "updated" triggers
|
The following placeholders are only available for "added" or "updated" triggers
|
||||||
|
|
||||||
@@ -627,7 +595,7 @@ The following placeholders are only available for "added" or "updated" triggers
|
|||||||
- `{{created_year_short}}`: created year
|
- `{{created_year_short}}`: created year
|
||||||
- `{{created_month}}`: created month
|
- `{{created_month}}`: created month
|
||||||
- `{{created_month_name}}`: created month name
|
- `{{created_month_name}}`: created month name
|
||||||
- `{{created_month_name_short}}`: created month short name
|
- `{created_month_name_short}}`: created month short name
|
||||||
- `{{created_day}}`: created day
|
- `{{created_day}}`: created day
|
||||||
- `{{created_time}}`: created time in HH:MM format
|
- `{{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_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
|
||||||
|
|||||||
87
mkdocs.yml
Normal file
87
mkdocs.yml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
site_name: Paperless-ngx
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
logo: assets/logo.svg
|
||||||
|
font:
|
||||||
|
text: Roboto
|
||||||
|
code: Roboto Mono
|
||||||
|
palette:
|
||||||
|
# Palette toggle for automatic mode
|
||||||
|
- media: "(prefers-color-scheme)"
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-auto
|
||||||
|
name: Switch to light mode
|
||||||
|
# Palette toggle for light mode
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
# Palette toggle for dark mode
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to system preference
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.top
|
||||||
|
- toc.integrate
|
||||||
|
- content.code.annotate
|
||||||
|
icon:
|
||||||
|
repo: fontawesome/brands/github
|
||||||
|
favicon: assets/favicon.png
|
||||||
|
repo_url: https://github.com/paperless-ngx/paperless-ngx
|
||||||
|
repo_name: paperless-ngx/paperless-ngx
|
||||||
|
edit_uri: blob/main/docs/
|
||||||
|
extra_css:
|
||||||
|
- assets/extra.css
|
||||||
|
markdown_extensions:
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
- def_list
|
||||||
|
- admonition
|
||||||
|
- tables
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.tilde
|
||||||
|
- footnotes
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||||
|
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||||
|
strict: true
|
||||||
|
nav:
|
||||||
|
- index.md
|
||||||
|
- setup.md
|
||||||
|
- 'Basic Usage': usage.md
|
||||||
|
- configuration.md
|
||||||
|
- administration.md
|
||||||
|
- advanced_usage.md
|
||||||
|
- 'REST API': api.md
|
||||||
|
- development.md
|
||||||
|
- 'FAQs': faq.md
|
||||||
|
- troubleshooting.md
|
||||||
|
- 'Migration to v3': migration.md
|
||||||
|
- changelog.md
|
||||||
|
copyright: Copyright © 2016 - 2026 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/paperless-ngx/paperless-ngx
|
||||||
|
- icon: fontawesome/brands/docker
|
||||||
|
link: https://hub.docker.com/r/paperlessngx/paperless-ngx
|
||||||
|
- icon: material/chat
|
||||||
|
link: https://matrix.to/#/#paperless:matrix.org
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- glightbox:
|
||||||
|
skip_classes:
|
||||||
|
- no-lightbox
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
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"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
@@ -26,30 +27,28 @@ dependencies = [
|
|||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.10",
|
"django~=5.2.10",
|
||||||
"django-allauth[mfa,socialaccount]~=65.14.0",
|
"django-allauth[mfa,socialaccount]~=65.13.1",
|
||||||
"django-auditlog~=3.4.1",
|
"django-auditlog~=3.4.1",
|
||||||
"django-cachalot~=2.9.0",
|
"django-cachalot~=2.8.0",
|
||||||
"django-celery-results~=2.6.0",
|
"django-celery-results~=2.6.0",
|
||||||
"django-compression-middleware~=0.5.0",
|
"django-compression-middleware~=0.5.0",
|
||||||
"django-cors-headers~=4.9.0",
|
"django-cors-headers~=4.9.0",
|
||||||
"django-extensions~=4.1",
|
"django-extensions~=4.1",
|
||||||
"django-filter~=25.1",
|
"django-filter~=25.1",
|
||||||
"django-guardian~=3.3.0",
|
"django-guardian~=3.2.0",
|
||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=1.0.1",
|
||||||
"django-rich~=2.2.0",
|
|
||||||
"django-soft-delete~=1.0.18",
|
"django-soft-delete~=1.0.18",
|
||||||
"django-treenode>=0.23.2",
|
"django-treenode>=0.23.2",
|
||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"djangorestframework-guardian~=0.4.0",
|
"djangorestframework-guardian~=0.4.0",
|
||||||
"drf-spectacular~=0.28",
|
"drf-spectacular~=0.28",
|
||||||
"drf-spectacular-sidecar~=2026.1.1",
|
"drf-spectacular-sidecar~=2025.10.1",
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"faiss-cpu>=1.10",
|
"faiss-cpu>=1.10",
|
||||||
"filelock~=3.20.3",
|
"filelock~=3.20.0",
|
||||||
"flower~=2.0.1",
|
"flower~=2.0.1",
|
||||||
"gotenberg-client~=0.13.1",
|
"gotenberg-client~=0.13.1",
|
||||||
"httpx-oauth~=0.16",
|
"httpx-oauth~=0.16",
|
||||||
"ijson>=3.2",
|
|
||||||
"imap-tools~=1.11.0",
|
"imap-tools~=1.11.0",
|
||||||
"jinja2~=3.1.5",
|
"jinja2~=3.1.5",
|
||||||
"langdetect~=1.0.9",
|
"langdetect~=1.0.9",
|
||||||
@@ -69,6 +68,7 @@ dependencies = [
|
|||||||
"python-gnupg~=0.5.4",
|
"python-gnupg~=0.5.4",
|
||||||
"python-ipware~=3.0.0",
|
"python-ipware~=3.0.0",
|
||||||
"python-magic~=0.4.27",
|
"python-magic~=0.4.27",
|
||||||
|
"pyzbar~=0.1.9",
|
||||||
"rapidfuzz~=3.14.0",
|
"rapidfuzz~=3.14.0",
|
||||||
"redis[hiredis]~=5.2.1",
|
"redis[hiredis]~=5.2.1",
|
||||||
"regex>=2025.9.18",
|
"regex>=2025.9.18",
|
||||||
@@ -76,11 +76,12 @@ dependencies = [
|
|||||||
"sentence-transformers>=4.1",
|
"sentence-transformers>=4.1",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"tika-client~=0.10.0",
|
"tika-client~=0.10.0",
|
||||||
"torch~=2.10.0",
|
"torch~=2.9.1",
|
||||||
|
"tqdm~=4.67.1",
|
||||||
"watchfiles>=1.1.1",
|
"watchfiles>=1.1.1",
|
||||||
"whitenoise~=6.11",
|
"whitenoise~=6.11",
|
||||||
"whoosh-reloaded>=2.7.5",
|
"whoosh-reloaded>=2.7.5",
|
||||||
"zxing-cpp~=3.0.0",
|
"zxing-cpp~=2.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
optional-dependencies.mariadb = [
|
optional-dependencies.mariadb = [
|
||||||
@@ -93,7 +94,7 @@ optional-dependencies.postgres = [
|
|||||||
"psycopg-pool==3.3",
|
"psycopg-pool==3.3",
|
||||||
]
|
]
|
||||||
optional-dependencies.webserver = [
|
optional-dependencies.webserver = [
|
||||||
"granian[uvloop]~=2.7.0",
|
"granian[uvloop]~=2.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@@ -105,18 +106,18 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
docs = [
|
docs = [
|
||||||
"zensical>=0.0.21",
|
"mkdocs-glightbox~=0.5.1",
|
||||||
|
"mkdocs-material~=9.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
testing = [
|
testing = [
|
||||||
"daphne",
|
"daphne",
|
||||||
"factory-boy~=3.3.1",
|
"factory-boy~=3.3.1",
|
||||||
"faker~=40.5.1",
|
|
||||||
"imagehash",
|
"imagehash",
|
||||||
"pytest~=9.0.0",
|
"pytest~=9.0.0",
|
||||||
"pytest-cov~=7.0.0",
|
"pytest-cov~=7.0.0",
|
||||||
"pytest-django~=4.12.0",
|
"pytest-django~=4.11.1",
|
||||||
"pytest-env~=1.5.0",
|
"pytest-env~=1.2.0",
|
||||||
"pytest-httpx",
|
"pytest-httpx",
|
||||||
"pytest-mock~=3.15.1",
|
"pytest-mock~=3.15.1",
|
||||||
#"pytest-randomly~=4.0.1",
|
#"pytest-randomly~=4.0.1",
|
||||||
@@ -126,8 +127,9 @@ testing = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
lint = [
|
lint = [
|
||||||
"prek~=0.3.0",
|
"pre-commit~=4.5.1",
|
||||||
"ruff~=0.15.0",
|
"pre-commit-uv~=4.2.0",
|
||||||
|
"ruff~=0.14.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
typing = [
|
typing = [
|
||||||
@@ -136,12 +138,8 @@ typing = [
|
|||||||
"django-stubs[compatible-mypy]",
|
"django-stubs[compatible-mypy]",
|
||||||
"djangorestframework-stubs[compatible-mypy]",
|
"djangorestframework-stubs[compatible-mypy]",
|
||||||
"lxml-stubs",
|
"lxml-stubs",
|
||||||
"microsoft-python-type-stubs @ git+https://github.com/microsoft/python-type-stubs.git",
|
|
||||||
"mypy",
|
"mypy",
|
||||||
"mypy-baseline",
|
|
||||||
"pyrefly",
|
|
||||||
"types-bleach",
|
"types-bleach",
|
||||||
"types-channels",
|
|
||||||
"types-colorama",
|
"types-colorama",
|
||||||
"types-dateparser",
|
"types-dateparser",
|
||||||
"types-markdown",
|
"types-markdown",
|
||||||
@@ -150,6 +148,7 @@ typing = [
|
|||||||
"types-pytz",
|
"types-pytz",
|
||||||
"types-redis",
|
"types-redis",
|
||||||
"types-setuptools",
|
"types-setuptools",
|
||||||
|
"types-tqdm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
@@ -160,24 +159,28 @@ environments = [
|
|||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[tool.uv.index]]
|
|
||||||
name = "pytorch-cpu"
|
|
||||||
url = "https://download.pytorch.org/whl/cpu"
|
|
||||||
explicit = true
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
# Markers are chosen to select these almost exclusively when building the Docker image
|
# Markers are chosen to select these almost exclusively when building the Docker image
|
||||||
psycopg-c = [
|
psycopg-c = [
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-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_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'" },
|
{ 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 = [
|
torch = [
|
||||||
{ index = "pytorch-cpu" },
|
{ index = "pytorch-cpu" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "pytorch-cpu"
|
||||||
|
url = "https://download.pytorch.org/whl/cpu"
|
||||||
|
explicit = true
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py310"
|
||||||
line-length = 88
|
line-length = 88
|
||||||
src = [
|
src = [
|
||||||
"src",
|
"src",
|
||||||
@@ -303,21 +306,11 @@ markers = [
|
|||||||
"gotenberg: Tests requiring Gotenberg service",
|
"gotenberg: Tests requiring Gotenberg service",
|
||||||
"tika: Tests requiring Tika service",
|
"tika: Tests requiring Tika service",
|
||||||
"greenmail: Tests requiring Greenmail 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]
|
[tool.pytest_env]
|
||||||
PAPERLESS_DISABLE_DBHANDLER = "true"
|
PAPERLESS_DISABLE_DBHANDLER = "true"
|
||||||
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
||||||
PAPERLESS_CHANNELS_BACKEND = "channels.layers.InMemoryChannelLayer"
|
|
||||||
|
|
||||||
[tool.coverage.report]
|
|
||||||
exclude_also = [
|
|
||||||
"if settings.AUDIT_LOG_ENABLED:",
|
|
||||||
"if AUDIT_LOG_ENABLED:",
|
|
||||||
"if TYPE_CHECKING:",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = [
|
source = [
|
||||||
@@ -330,6 +323,13 @@ omit = [
|
|||||||
"paperless/auth.py",
|
"paperless/auth.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_also = [
|
||||||
|
"if settings.AUDIT_LOG_ENABLED:",
|
||||||
|
"if AUDIT_LOG_ENABLED:",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
mypy_path = "src"
|
mypy_path = "src"
|
||||||
plugins = [
|
plugins = [
|
||||||
@@ -343,15 +343,5 @@ disallow_untyped_defs = true
|
|||||||
warn_redundant_casts = true
|
warn_redundant_casts = true
|
||||||
warn_unused_ignores = true
|
warn_unused_ignores = true
|
||||||
|
|
||||||
[tool.pyrefly]
|
|
||||||
search-path = [ "src" ]
|
|
||||||
baseline = ".pyrefly-baseline.json"
|
|
||||||
python-platform = "linux"
|
|
||||||
|
|
||||||
[tool.django-stubs]
|
[tool.django-stubs]
|
||||||
django_settings_module = "paperless.settings"
|
django_settings_module = "paperless.settings"
|
||||||
|
|
||||||
[tool.mypy-baseline]
|
|
||||||
baseline_path = ".mypy-baseline.txt"
|
|
||||||
sort_baseline = true
|
|
||||||
ignore_categories = [ "note" ]
|
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ following additional information about it:
|
|||||||
* Correspondent: ${DOCUMENT_CORRESPONDENT}
|
* Correspondent: ${DOCUMENT_CORRESPONDENT}
|
||||||
* Tags: ${DOCUMENT_TAGS}
|
* Tags: ${DOCUMENT_TAGS}
|
||||||
|
|
||||||
|
It was consumed with the passphrase ${PASSPHRASE}
|
||||||
|
|
||||||
"
|
"
|
||||||
|
|||||||
51
src-ui/.eslintrc.json
Normal file
51
src-ui/.eslintrc.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
|
"ng2-pdf-viewer",
|
||||||
"file-saver",
|
"file-saver",
|
||||||
"utif"
|
"utif"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ test('dashboard saved view document links', async ({ page }) => {
|
|||||||
test('test slim sidebar', async ({ page }) => {
|
test('test slim sidebar', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.locator('.sidebar-slim-toggler').click()
|
await page.locator('#sidebarMenu').getByRole('button').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
await page.locator('.sidebar-slim-toggler').click()
|
await page.locator('#sidebarMenu').getByRole('button').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"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,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"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,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"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,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"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,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ test('should show a mobile preview', async ({ page }) => {
|
|||||||
await page.setViewportSize({ width: 400, height: 1000 })
|
await page.setViewportSize({ width: 400, height: 1000 })
|
||||||
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
||||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||||
await page.waitForSelector('pngx-pdf-viewer')
|
await page.waitForSelector('pdf-viewer')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should show a list of notes', async ({ page }) => {
|
test('should show a list of notes', async ({ page }) => {
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Correspondents' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/correspondents')
|
await page.goto('/correspondents')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/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 }) => {
|
test('should not allow user to view tags', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
await page.goto('/tags')
|
||||||
).not.toBeAttached()
|
|
||||||
await page.goto('/attributes/tags')
|
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/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.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Document Types' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/documenttypes')
|
await page.goto('/documenttypes')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/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.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Storage Paths' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/storagepaths')
|
await page.goto('/storagepaths')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -31,10 +31,6 @@ module.exports = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
...esmPreset.moduleNameMapper,
|
...esmPreset.moduleNameMapper,
|
||||||
'^src/(.*)': '<rootDir>/src/$1',
|
'^src/(.*)': '<rootDir>/src/$1',
|
||||||
'^pdfjs-dist/legacy/build/pdf\\.mjs$':
|
|
||||||
'<rootDir>/src/test/mocks/pdfjs-legacy-build-pdf.ts',
|
|
||||||
'^pdfjs-dist/web/pdf_viewer\\.mjs$':
|
|
||||||
'<rootDir>/src/test/mocks/pdfjs-web-pdf_viewer.ts',
|
|
||||||
},
|
},
|
||||||
workerIdleMemoryLimit: '512MB',
|
workerIdleMemoryLimit: '512MB',
|
||||||
reporters: [
|
reporters: [
|
||||||
|
|||||||
2793
src-ui/messages.xlf
2793
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "2.20.10",
|
"version": "2.20.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -11,64 +11,64 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^21.2.0",
|
"@angular/cdk": "^21.1.2",
|
||||||
"@angular/common": "~21.2.0",
|
"@angular/common": "~21.1.2",
|
||||||
"@angular/compiler": "~21.2.0",
|
"@angular/compiler": "~21.1.2",
|
||||||
"@angular/core": "~21.2.0",
|
"@angular/core": "~21.1.2",
|
||||||
"@angular/forms": "~21.2.0",
|
"@angular/forms": "~21.1.2",
|
||||||
"@angular/localize": "~21.2.0",
|
"@angular/localize": "~21.1.2",
|
||||||
"@angular/platform-browser": "~21.2.0",
|
"@angular/platform-browser": "~21.1.2",
|
||||||
"@angular/platform-browser-dynamic": "~21.2.0",
|
"@angular/platform-browser-dynamic": "~21.1.2",
|
||||||
"@angular/router": "~21.2.0",
|
"@angular/router": "~21.1.2",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
|
"@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",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
|
"ng2-pdf-viewer": "^10.4.0",
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^10.1.0",
|
"ngx-color": "^10.1.0",
|
||||||
"ngx-cookie-service": "^21.1.0",
|
"ngx-cookie-service": "^21.1.0",
|
||||||
"ngx-device-detector": "^11.0.0",
|
"ngx-device-detector": "^11.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^18.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
|
||||||
"pdfjs-dist": "^5.4.624",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"zone.js": "^0.16.1"
|
"zone.js": "^0.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^21.0.3",
|
"@angular-builders/custom-webpack": "^21.0.3",
|
||||||
"@angular-builders/jest": "^21.0.3",
|
"@angular-builders/jest": "^21.0.3",
|
||||||
"@angular-devkit/core": "^21.2.0",
|
"@angular-devkit/core": "^21.1.2",
|
||||||
"@angular-devkit/schematics": "^21.2.0",
|
"@angular-devkit/schematics": "^21.1.2",
|
||||||
"@angular-eslint/builder": "21.3.0",
|
"@angular-eslint/builder": "21.2.0",
|
||||||
"@angular-eslint/eslint-plugin": "21.3.0",
|
"@angular-eslint/eslint-plugin": "21.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "21.3.0",
|
"@angular-eslint/eslint-plugin-template": "21.2.0",
|
||||||
"@angular-eslint/schematics": "21.3.0",
|
"@angular-eslint/schematics": "21.2.0",
|
||||||
"@angular-eslint/template-parser": "21.3.0",
|
"@angular-eslint/template-parser": "21.2.0",
|
||||||
"@angular/build": "^21.2.0",
|
"@angular/build": "^21.1.2",
|
||||||
"@angular/cli": "~21.2.0",
|
"@angular/cli": "~21.1.2",
|
||||||
"@angular/compiler-cli": "~21.2.0",
|
"@angular/compiler-cli": "~21.1.2",
|
||||||
"@codecov/webpack-plugin": "^1.9.1",
|
"@codecov/webpack-plugin": "^1.9.1",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.1",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"@typescript-eslint/utils": "^8.54.0",
|
"@typescript-eslint/utils": "^8.54.0",
|
||||||
"eslint": "^10.0.2",
|
"eslint": "^9.39.2",
|
||||||
"jest": "30.2.0",
|
"jest": "30.2.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"jest-preset-angular": "^16.1.1",
|
"jest-preset-angular": "^16.0.0",
|
||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"webpack": "^5.105.3"
|
"webpack": "^5.103.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.17.1",
|
"packageManager": "pnpm@10.17.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
3398
src-ui/pnpm-lock.yaml
generated
3398
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -100,10 +100,10 @@ const mock = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(globalThis, 'open', { value: jest.fn() })
|
Object.defineProperty(window, 'open', { value: jest.fn() })
|
||||||
Object.defineProperty(globalThis, 'localStorage', { value: mock() })
|
Object.defineProperty(window, 'localStorage', { value: mock() })
|
||||||
Object.defineProperty(globalThis, 'sessionStorage', { value: mock() })
|
Object.defineProperty(window, 'sessionStorage', { value: mock() })
|
||||||
Object.defineProperty(globalThis, 'getComputedStyle', {
|
Object.defineProperty(window, 'getComputedStyle', {
|
||||||
value: () => ['-webkit-appearance'],
|
value: () => ['-webkit-appearance'],
|
||||||
})
|
})
|
||||||
Object.defineProperty(navigator, 'clipboard', {
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
@@ -115,33 +115,13 @@ Object.defineProperty(navigator, 'canShare', { value: () => true })
|
|||||||
if (!navigator.share) {
|
if (!navigator.share) {
|
||||||
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
if (!globalThis.URL.createObjectURL) {
|
if (!URL.createObjectURL) {
|
||||||
Object.defineProperty(globalThis.URL, 'createObjectURL', { value: jest.fn() })
|
Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
if (!globalThis.URL.revokeObjectURL) {
|
if (!URL.revokeObjectURL) {
|
||||||
Object.defineProperty(globalThis.URL, 'revokeObjectURL', { value: jest.fn() })
|
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
class MockResizeObserver {
|
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||||
private readonly callback: ResizeObserverCallback
|
|
||||||
|
|
||||||
constructor(callback: ResizeObserverCallback) {
|
|
||||||
this.callback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
observe = jest.fn()
|
|
||||||
unobserve = jest.fn()
|
|
||||||
disconnect = jest.fn()
|
|
||||||
|
|
||||||
trigger = (entries: ResizeObserverEntry[] = []) => {
|
|
||||||
this.callback(entries, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.defineProperty(globalThis, 'ResizeObserver', {
|
|
||||||
writable: true,
|
|
||||||
configurable: true,
|
|
||||||
value: MockResizeObserver,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (typeof IntersectionObserver === 'undefined') {
|
if (typeof IntersectionObserver === 'undefined') {
|
||||||
class MockIntersectionObserver {
|
class MockIntersectionObserver {
|
||||||
@@ -156,7 +136,7 @@ if (typeof IntersectionObserver === 'undefined') {
|
|||||||
takeRecords = jest.fn()
|
takeRecords = jest.fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(globalThis, 'IntersectionObserver', {
|
Object.defineProperty(window, 'IntersectionObserver', {
|
||||||
writable: true,
|
writable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: MockIntersectionObserver,
|
value: MockIntersectionObserver,
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
|
|||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.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 { MailComponent } from './components/manage/mail/mail.component'
|
||||||
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.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 { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
@@ -101,77 +105,53 @@ export const routes: Routes = [
|
|||||||
componentName: 'DocumentAsnComponent',
|
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',
|
path: 'tags',
|
||||||
redirectTo: '/attributes/tags',
|
component: TagListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
},
|
data: {
|
||||||
{
|
requiredPermission: {
|
||||||
path: 'correspondents',
|
action: PermissionAction.View,
|
||||||
redirectTo: '/attributes/correspondents',
|
type: PermissionType.Tag,
|
||||||
pathMatch: 'full',
|
},
|
||||||
|
componentName: 'TagListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'documenttypes',
|
path: 'documenttypes',
|
||||||
redirectTo: '/attributes/documenttypes',
|
component: DocumentTypeListComponent,
|
||||||
pathMatch: 'full',
|
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',
|
path: 'storagepaths',
|
||||||
redirectTo: '/attributes/storagepaths',
|
component: StoragePathListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.StoragePath,
|
||||||
|
},
|
||||||
|
componentName: 'StoragePathListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
@@ -259,8 +239,15 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'customfields',
|
path: 'customfields',
|
||||||
redirectTo: '/attributes/customfields',
|
component: CustomFieldsComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.CustomField,
|
||||||
|
},
|
||||||
|
componentName: 'CustomFieldsComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflows',
|
path: 'workflows',
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import {
|
|||||||
import { Router, RouterModule } from '@angular/router'
|
import { Router, RouterModule } from '@angular/router'
|
||||||
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import {
|
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
provideUiTour,
|
|
||||||
TourNgBootstrap,
|
|
||||||
TourService,
|
|
||||||
} from 'ngx-ui-tour-ng-bootstrap'
|
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { routes } from './app-routing.module'
|
import { routes } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
@@ -44,12 +40,12 @@ describe('AppComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
TourNgBootstrapModule,
|
||||||
RouterModule.forRoot(routes),
|
RouterModule.forRoot(routes),
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
AppComponent,
|
AppComponent,
|
||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
FileDropComponent,
|
FileDropComponent,
|
||||||
TourNgBootstrap,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -57,7 +53,6 @@ describe('AppComponent', () => {
|
|||||||
DirtySavedViewGuard,
|
DirtySavedViewGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
provideUiTour(),
|
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||||
import { Router, RouterOutlet } from '@angular/router'
|
import { Router, RouterOutlet } from '@angular/router'
|
||||||
import { TourNgBootstrap, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { first, Subscription } from 'rxjs'
|
import { first, Subscription } from 'rxjs'
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||||
@@ -21,7 +21,12 @@ import { WebsocketStatusService } from './services/websocket-status.service'
|
|||||||
selector: 'pngx-root',
|
selector: 'pngx-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
imports: [FileDropComponent, ToastsComponent, TourNgBootstrap, RouterOutlet],
|
imports: [
|
||||||
|
FileDropComponent,
|
||||||
|
ToastsComponent,
|
||||||
|
TourNgBootstrapModule,
|
||||||
|
RouterOutlet,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private settings = inject(SettingsService)
|
private settings = inject(SettingsService)
|
||||||
@@ -162,91 +167,108 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tourService.initialize([
|
const prevBtnTitle = $localize`Prev`
|
||||||
{
|
const nextBtnTitle = $localize`Next`
|
||||||
anchorId: 'tour.dashboard',
|
const endBtnTitle = $localize`End`
|
||||||
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
|
|
||||||
route: '/dashboard',
|
this.tourService.initialize(
|
||||||
delayAfterNavigation: 500,
|
[
|
||||||
isOptional: false,
|
{
|
||||||
},
|
anchorId: 'tour.dashboard',
|
||||||
{
|
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
|
||||||
anchorId: 'tour.upload-widget',
|
route: '/dashboard',
|
||||||
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
|
delayAfterNavigation: 500,
|
||||||
route: '/dashboard',
|
isOptional: false,
|
||||||
},
|
|
||||||
{
|
|
||||||
anchorId: 'tour.documents',
|
|
||||||
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
|
|
||||||
route: '/documents?sort=created&reverse=1&page=1',
|
|
||||||
delayAfterNavigation: 500,
|
|
||||||
placement: 'bottom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
anchorId: 'tour.documents-filter-editor',
|
|
||||||
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
|
|
||||||
route: '/documents?sort=created&reverse=1&page=1',
|
|
||||||
placement: 'bottom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
anchorId: 'tour.documents-views',
|
|
||||||
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
|
|
||||||
route: '/documents?sort=created&reverse=1&page=1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
backdropConfig: {
|
|
||||||
offset: 0,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
anchorId: 'tour.upload-widget',
|
||||||
anchorId: 'tour.mail',
|
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
|
||||||
content: $localize`Manage e-mail accounts and rules for automatically importing documents.`,
|
route: '/dashboard',
|
||||||
route: '/mail',
|
|
||||||
backdropConfig: {
|
|
||||||
offset: 0,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
anchorId: 'tour.documents',
|
||||||
anchorId: 'tour.workflows',
|
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
|
||||||
content: $localize`Workflows give you more control over the document pipeline.`,
|
route: '/documents?sort=created&reverse=1&page=1',
|
||||||
route: '/workflows',
|
delayAfterNavigation: 500,
|
||||||
backdropConfig: {
|
placement: 'bottom',
|
||||||
offset: 0,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
anchorId: 'tour.documents-filter-editor',
|
||||||
anchorId: 'tour.file-tasks',
|
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
|
||||||
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
|
route: '/documents?sort=created&reverse=1&page=1',
|
||||||
route: '/tasks',
|
placement: 'bottom',
|
||||||
backdropConfig: {
|
|
||||||
offset: 0,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
anchorId: 'tour.documents-views',
|
||||||
anchorId: 'tour.settings',
|
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
|
||||||
content: $localize`Check out the settings for various tweaks to the web app.`,
|
route: '/documents?sort=created&reverse=1&page=1',
|
||||||
route: '/settings',
|
|
||||||
backdropConfig: {
|
|
||||||
offset: 0,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
anchorId: 'tour.tags',
|
||||||
anchorId: 'tour.outro',
|
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.`,
|
||||||
title: $localize`Thank you! 🙏`,
|
route: '/tags',
|
||||||
content:
|
backdropConfig: {
|
||||||
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
|
offset: 0,
|
||||||
'<br/><br/>' +
|
},
|
||||||
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
|
|
||||||
route: '/dashboard',
|
|
||||||
isOptional: false,
|
|
||||||
backdropConfig: {
|
|
||||||
offset: 0,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
anchorId: 'tour.mail',
|
||||||
|
content: $localize`Manage e-mail accounts and rules for automatically importing documents.`,
|
||||||
|
route: '/mail',
|
||||||
|
backdropConfig: {
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.workflows',
|
||||||
|
content: $localize`Workflows give you more control over the document pipeline.`,
|
||||||
|
route: '/workflows',
|
||||||
|
backdropConfig: {
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.file-tasks',
|
||||||
|
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
|
||||||
|
route: '/tasks',
|
||||||
|
backdropConfig: {
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.settings',
|
||||||
|
content: $localize`Check out the settings for various tweaks to the web app.`,
|
||||||
|
route: '/settings',
|
||||||
|
backdropConfig: {
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.outro',
|
||||||
|
title: $localize`Thank you! 🙏`,
|
||||||
|
content:
|
||||||
|
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
|
||||||
|
'<br/><br/>' +
|
||||||
|
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
|
||||||
|
route: '/dashboard',
|
||||||
|
isOptional: false,
|
||||||
|
backdropConfig: {
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
enableBackdrop: true,
|
||||||
|
backdropConfig: {
|
||||||
|
offset: 10,
|
||||||
|
},
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
isOptional: true,
|
||||||
|
useLegacyTitle: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
this.tourService.start$.subscribe(() => {
|
this.tourService.start$.subscribe(() => {
|
||||||
this.renderer.addClass(document.body, 'tour-active')
|
this.renderer.addClass(document.body, 'tour-active')
|
||||||
|
|||||||
@@ -19,18 +19,13 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card bg-light">
|
<div class="card bg-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title d-flex align-items-center">
|
<div class="card-title">
|
||||||
<h6 class="mb-0">
|
<h6>
|
||||||
{{option.title}}
|
{{option.title}}
|
||||||
|
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
||||||
|
<i-bs name="info-circle"></i-bs>
|
||||||
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
|
||||||
<i-bs name="info-circle"></i-bs>
|
|
||||||
</a>
|
|
||||||
@if (isSet(option.key)) {
|
|
||||||
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
|
|
||||||
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-n3">
|
<div class="mb-n3">
|
||||||
@switch (option.type) {
|
@switch (option.type) {
|
||||||
|
|||||||
@@ -144,18 +144,4 @@ describe('ConfigComponent', () => {
|
|||||||
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
expect(initSpy).toHaveBeenCalled()
|
expect(initSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reset option to null', () => {
|
|
||||||
component.configForm.patchValue({ output_type: OutputTypeConfig.PDF_A })
|
|
||||||
expect(component.isSet('output_type')).toBeTruthy()
|
|
||||||
component.resetOption('output_type')
|
|
||||||
expect(component.configForm.get('output_type').value).toBeNull()
|
|
||||||
expect(component.isSet('output_type')).toBeFalsy()
|
|
||||||
component.configForm.patchValue({ app_title: 'Test Title' })
|
|
||||||
component.resetOption('app_title')
|
|
||||||
expect(component.configForm.get('app_title').value).toBeNull()
|
|
||||||
component.configForm.patchValue({ barcodes_enabled: true })
|
|
||||||
component.resetOption('barcodes_enabled')
|
|
||||||
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
i18n-info
|
i18n-info
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
<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> <ng-container i18n>Start tour</ng-container>
|
||||||
</button>
|
</button>
|
||||||
@if (permissionsService.isAdmin()) {
|
@if (permissionsService.isAdmin()) {
|
||||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||||
[disabled]="!systemStatus">
|
[disabled]="!systemStatus">
|
||||||
@if (!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 {
|
} @else {
|
||||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||||
@if (systemStatusHasErrors) {
|
@if (systemStatusHasErrors) {
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||||
<ng-container i18n>Open Django Admin</ng-container>
|
<ng-container i18n>Open Django Admin</ng-container>
|
||||||
<i-bs class="ms-2" name="arrow-up-right"></i-bs>
|
<i-bs name="arrow-up-right"></i-bs>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
@@ -222,8 +222,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
||||||
<option [ngValue]="PdfZoomScale.PageWidth" i18n>Fit width</option>
|
<option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option>
|
||||||
<option [ngValue]="PdfZoomScale.PageFit" i18n>Fit page</option>
|
<option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="pdfEditorDefaultEditMode">
|
<select class="form-select" formControlName="pdfEditorDefaultEditMode">
|
||||||
<option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import {
|
import {
|
||||||
@@ -148,7 +147,6 @@ describe('SettingsComponent', () => {
|
|||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
provideUiTour(),
|
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
|
|||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
import { PdfZoomScale } from '../../common/pdf-viewer/pdf-viewer.types'
|
|
||||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
|
import { ZoomSetting } from '../../document-detail/zoom-setting'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
@@ -196,7 +196,7 @@ export class SettingsComponent
|
|||||||
|
|
||||||
public readonly GlobalSearchType = GlobalSearchType
|
public readonly GlobalSearchType = GlobalSearchType
|
||||||
|
|
||||||
public readonly PdfZoomScale = PdfZoomScale
|
public readonly ZoomSetting = ZoomSetting
|
||||||
|
|
||||||
public readonly PdfEditorEditMode = PdfEditorEditMode
|
public readonly PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
>
|
>
|
||||||
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
<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">
|
<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> <ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
<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> {{dismissButtonText}}
|
||||||
</button>
|
</button>
|
||||||
<div class="form-inline d-flex align-items-center">
|
<div class="form-inline d-flex align-items-center">
|
||||||
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||||
@@ -113,12 +113,12 @@
|
|||||||
<td scope="row">
|
<td scope="row">
|
||||||
<div class="btn-group" role="group">
|
<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 }">
|
<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> <ng-container i18n>Dismiss</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
@if (task.related_document) {
|
@if (task.related_document) {
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
<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> <ng-container i18n>Open Document</ng-container>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
i18n-info
|
i18n-info
|
||||||
infoLink="usage/#document-trash">
|
infoLink="usage/#document-trash">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments.size === 0">
|
<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> <ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
|
<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> <ng-container i18n>Restore selected</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
|
<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> <ng-container i18n>Delete selected</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash.length === 0">
|
<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> <ng-container i18n>Empty trash</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@@ -75,10 +75,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="btn-group d-none d-sm-block">
|
<div class="btn-group d-none d-sm-block">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();">
|
<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> <ng-container i18n>Restore</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();">
|
<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> <ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<h4 class="d-flex">
|
<h4 class="d-flex">
|
||||||
<ng-container i18n>Users</ng-container>
|
<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 }">
|
<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> <ng-container i18n>Add User</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
<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> <ng-container i18n>Edit</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
<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> <ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<h4 class="mt-4 d-flex">
|
<h4 class="mt-4 d-flex">
|
||||||
<ng-container i18n>Groups</ng-container>
|
<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 }">
|
<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> <ng-container i18n>Add Group</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@@ -70,10 +70,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.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> <ng-container i18n>Edit</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
<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> <ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,20 +86,25 @@
|
|||||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Dashboard</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Documents</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
<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">
|
<h6 class="sidebar-heading px-3 text-muted">
|
||||||
<span i18n>Saved views</span>
|
<span i18n>Saved views</span>
|
||||||
</h6>
|
</h6>
|
||||||
@@ -112,7 +117,8 @@
|
|||||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
||||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
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> <div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
|
||||||
@if (showSidebarCounts && !slimSidebarEnabled) {
|
@if (showSidebarCounts && !slimSidebarEnabled) {
|
||||||
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
||||||
}
|
}
|
||||||
@@ -129,11 +135,6 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
|
||||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
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> {{d.title | documentTitle}}</span>
|
||||||
<span class="close flex-column justify-content-center" (click)="closeDocument(d); $event.preventDefault()">
|
<span class="close flex-column justify-content-center" (click)="closeDocument(d); $event.preventDefault()">
|
||||||
<i-bs name="x"></i-bs>
|
<i-bs name="x"></i-bs>
|
||||||
</span>
|
</span>
|
||||||
@@ -162,7 +163,7 @@
|
|||||||
<a class="nav-link app-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
<a class="nav-link app-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
|
||||||
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Close all</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -174,65 +175,49 @@
|
|||||||
<span i18n>Manage</span>
|
<span i18n>Manage</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
@if (canManageAttributes) {
|
<li class="nav-item app-link"
|
||||||
<li class="nav-item app-link" tourAnchor="tour.tags">
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
<div class="d-flex align-items-center attributes-row">
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<a class="nav-link flex-fill" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()"
|
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
<i-bs class="me-2" name="stack"></i-bs><span><ng-container i18n>Attributes</ng-container></span>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
@if (!slimSidebarEnabled) {
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
||||||
<button
|
tourAnchor="tour.tags">
|
||||||
type="button"
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
||||||
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
(click)="toggleAttributesSections($event)"
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
|
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
i18n-aria-label
|
</a>
|
||||||
>
|
</li>
|
||||||
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
|
<li class="nav-item app-link"
|
||||||
</button>
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
}
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
||||||
</div>
|
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
<div
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
class="attributes-submenu ms-2"
|
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
||||||
[ngbCollapse]="slimSidebarEnabled || attributesSectionsCollapsed"
|
</a>
|
||||||
>
|
</li>
|
||||||
<ul class="nav flex-column">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
|
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<a class="nav-link py-1" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
|
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
<i-bs class="me-2" name="tags"></i-bs><span><ng-container i18n>Tags</ng-container></span>
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
</a>
|
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||||
</li>
|
</a>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
</li>
|
||||||
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<i-bs class="me-2" name="person"></i-bs><span><ng-container i18n>Correspondents</ng-container></span>
|
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||||
</a>
|
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
</li>
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||||
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
</a>
|
||||||
<i-bs class="me-2" name="hash"></i-bs><span><ng-container i18n>Document types</ng-container></span>
|
</li>
|
||||||
</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.SavedView }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Saved Views</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link"
|
<li class="nav-item app-link"
|
||||||
@@ -241,7 +226,7 @@
|
|||||||
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Workflows</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
|
<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"
|
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Mail</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
|
<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"
|
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Trash</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -272,21 +257,21 @@
|
|||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Settings</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
||||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Configuration</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Users & Groups</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link"
|
<li class="nav-item app-link"
|
||||||
@@ -295,7 +280,7 @@
|
|||||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <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><span class="badge bg-danger ms-2 d-inline">{{tasksService.failedFileTasks.length}}</span></span>
|
||||||
}</span>
|
}</span>
|
||||||
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
|
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
|
||||||
@@ -308,7 +293,7 @@
|
|||||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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> <ng-container i18n>Logs</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -317,7 +302,7 @@
|
|||||||
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
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"> <ng-container i18n>Documentation</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
@@ -356,9 +341,9 @@
|
|||||||
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
|
||||||
container="body">
|
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) {
|
@if (appRemoteVersion?.update_available) {
|
||||||
<ng-container i18n>Update available</ng-container>
|
<ng-container i18n>Update available</ng-container>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
.sidebar-heading {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -290,7 +281,7 @@ main {
|
|||||||
.navbar .dropdown-menu {
|
.navbar .dropdown-menu {
|
||||||
font-size: 0.875rem; // body size
|
font-size: 0.875rem; // body size
|
||||||
|
|
||||||
a i-bs, button i-bs {
|
a i-bs {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { ActivatedRoute, Router } from '@angular/router'
|
|||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
|
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
@@ -28,10 +27,7 @@ import {
|
|||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import {
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
PermissionType,
|
|
||||||
PermissionsService,
|
|
||||||
} from 'src/app/services/permissions.service'
|
|
||||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
@@ -161,7 +157,6 @@ describe('AppFrameComponent', () => {
|
|||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
provideUiTour(),
|
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@@ -243,19 +238,9 @@ describe('AppFrameComponent', () => {
|
|||||||
|
|
||||||
it('should support toggling slim sidebar and saving', fakeAsync(() => {
|
it('should support toggling slim sidebar and saving', fakeAsync(() => {
|
||||||
const saveSettingSpy = jest.spyOn(settingsService, 'set')
|
const saveSettingSpy = jest.spyOn(settingsService, 'set')
|
||||||
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
|
|
||||||
expect(component.slimSidebarEnabled).toBeFalsy()
|
expect(component.slimSidebarEnabled).toBeFalsy()
|
||||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||||
component.toggleSlimSidebar()
|
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()
|
expect(component.slimSidebarAnimating).toBeTruthy()
|
||||||
tick(200)
|
tick(200)
|
||||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||||
@@ -264,10 +249,6 @@ describe('AppFrameComponent', () => {
|
|||||||
SETTINGS_KEYS.SLIM_SIDEBAR,
|
SETTINGS_KEYS.SLIM_SIDEBAR,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
expect(saveSettingSpy).toHaveBeenCalledWith(
|
|
||||||
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
|
||||||
['attributes']
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
it('should show error on toggle slim sidebar if store settings fails', () => {
|
it('should show error on toggle slim sidebar if store settings fails', () => {
|
||||||
@@ -275,7 +256,7 @@ describe('AppFrameComponent', () => {
|
|||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
component.toggleSlimSidebar()
|
component.toggleSlimSidebar()
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.match(`${environment.apiBaseUrl}ui_settings/`)[0]
|
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||||
.flush('error', {
|
.flush('error', {
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: 'error',
|
statusText: 'error',
|
||||||
@@ -390,103 +371,4 @@ describe('AppFrameComponent', () => {
|
|||||||
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
||||||
expect(maybeRefreshSpy).toHaveBeenCalled()
|
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import {
|
|||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
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 { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||||
@@ -69,7 +69,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
TourNgBootstrap,
|
TourNgBootstrapModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppFrameComponent
|
export class AppFrameComponent
|
||||||
@@ -140,35 +140,12 @@ export class AppFrameComponent
|
|||||||
|
|
||||||
toggleSlimSidebar(): void {
|
toggleSlimSidebar(): void {
|
||||||
this.slimSidebarAnimating = true
|
this.slimSidebarAnimating = true
|
||||||
const slimSidebarEnabled = !this.slimSidebarEnabled
|
this.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)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.slimSidebarAnimating = false
|
this.slimSidebarAnimating = false
|
||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAttributesSections(event?: Event): void {
|
|
||||||
event?.preventDefault()
|
|
||||||
event?.stopPropagation()
|
|
||||||
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
get versionString(): string {
|
get versionString(): string {
|
||||||
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
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 {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
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 {
|
get aiEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,13 +49,17 @@
|
|||||||
[disabled]="disablePrimaryButton(type, item)"
|
[disabled]="disablePrimaryButton(type, item)"
|
||||||
(mouseenter)="onButtonHover($event)">
|
(mouseenter)="onButtonHover($event)">
|
||||||
@if (type === DataType.Document) {
|
@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> <ng-container i18n>Open</ng-container></span>
|
||||||
} @else if (type === DataType.SavedView) {
|
} @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> <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) {
|
} @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> <ng-container i18n>Open</ng-container></span>
|
||||||
} @else {
|
} @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> <ng-container i18n>Filter documents</ng-container></span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) {
|
@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)"
|
[disabled]="disableSecondaryButton(type, item)"
|
||||||
(mouseenter)="onButtonHover($event)">
|
(mouseenter)="onButtonHover($event)">
|
||||||
@if (type === DataType.Document) {
|
@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> <ng-container i18n>Download</ng-container></span>
|
||||||
} @else {
|
} @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> <ng-container i18n>Open</ng-container></span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,10 @@
|
|||||||
<ul class="list-group"
|
<ul class="list-group"
|
||||||
cdkDropList
|
cdkDropList
|
||||||
(cdkDropListDropped)="onDrop($event)">
|
(cdkDropListDropped)="onDrop($event)">
|
||||||
@for (document of documents; track document.id) {
|
@for (documentID of documentIDs; track documentID) {
|
||||||
<li class="list-group-item d-flex align-items-center" cdkDrag>
|
<li class="list-group-item" cdkDrag>
|
||||||
<i-bs name="grip-vertical" class="me-2"></i-bs>
|
<i-bs name="grip-vertical" class="me-2"></i-bs>
|
||||||
<div class="d-flex flex-column">
|
{{getDocument(documentID)?.title}}
|
||||||
<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>
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ import {
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
moveItemInArray,
|
moveItemInArray,
|
||||||
} from '@angular/cdk/drag-drop'
|
} from '@angular/cdk/drag-drop'
|
||||||
import { AsyncPipe } from '@angular/common'
|
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { takeUntil } from 'rxjs'
|
import { takeUntil } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
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 { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
@@ -20,9 +17,6 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|||||||
templateUrl: './merge-confirm-dialog.component.html',
|
templateUrl: './merge-confirm-dialog.component.html',
|
||||||
styleUrl: './merge-confirm-dialog.component.scss',
|
styleUrl: './merge-confirm-dialog.component.scss',
|
||||||
imports: [
|
imports: [
|
||||||
AsyncPipe,
|
|
||||||
CorrespondentNamePipe,
|
|
||||||
CustomDatePipe,
|
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
@for (docId of value; track docId) {
|
@for (docId of value; track docId) {
|
||||||
@if (getDocumentTitle(docId)) {
|
@if (getDocumentTitle(docId)) {
|
||||||
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
|
<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> <span>{{ getDocumentTitle(docId) }}</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions">
|
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
<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"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
||||||
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
|
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
@if (!filterText?.length || filteredFields.length === 0) {
|
@if (!filterText?.length || filteredFields.length === 0) {
|
||||||
<button class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
|
<button class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
|
||||||
<small>
|
<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> <ng-container i18n>Create new field</ng-container>
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
@if (useDropdown) {
|
@if (useDropdown) {
|
||||||
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
|
<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">
|
<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"> {{title}}</div>
|
||||||
@if (isActive) {
|
@if (isActive) {
|
||||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<div class="btn-group w-100" ngbDropdown role="group" [popperOptions]="popperOptions" [placement]="placement">
|
<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">
|
<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"> {{title}}</div>
|
||||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
@switch (objectForm.get('data_type').value) {
|
@switch (objectForm.get('data_type').value) {
|
||||||
@case (CustomFieldDataType.Select) {
|
@case (CustomFieldDataType.Select) {
|
||||||
<button type="button" class="btn btn-sm btn-primary my-2" (click)="addSelectOption()">
|
<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> <i-bs name="plus-circle"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
<div formArrayName="select_options">
|
<div formArrayName="select_options">
|
||||||
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
|
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
|
||||||
|
|||||||
@@ -9,24 +9,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<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>
|
<pngx-input-text [horizontal]="true" i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
</div>
|
</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>
|
<pngx-input-select [horizontal]="true" i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select>
|
||||||
</div>
|
</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">
|
<div class="col-md-2 pt-2">
|
||||||
<pngx-input-switch [horizontal]="true" i18n-title title="Enabled" formControlName="enabled"></pngx-input-switch>
|
<pngx-input-switch [horizontal]="true" i18n-title title="Enabled" formControlName="enabled"></pngx-input-switch>
|
||||||
</div>
|
</div>
|
||||||
</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"/>
|
<hr class="mt-0"/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the criteria specified below.</p>
|
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the criteria specified below.</p>
|
||||||
|
|||||||
@@ -222,7 +222,6 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
|
|||||||
),
|
),
|
||||||
assign_correspondent: new FormControl(null),
|
assign_correspondent: new FormControl(null),
|
||||||
assign_owner_from_rule: new FormControl(true),
|
assign_owner_from_rule: new FormControl(true),
|
||||||
stop_processing: new FormControl(false),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<p class="p-2" i18n>Trigger Workflow On:</p>
|
<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()">
|
<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> <ng-container i18n>Add Trigger</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ngbAccordion [closeOthers]="true">
|
<div ngbAccordion [closeOthers]="true">
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<p class="p-2" i18n>Apply Actions:</p>
|
<p class="p-2" i18n>Apply Actions:</p>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
|
<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> <ng-container i18n>Add Action</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
(click)="addFilter(formGroup)"
|
(click)="addFilter(formGroup)"
|
||||||
[disabled]="!canAddFilter(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> <span i18n>Add filter</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul class="mt-2 list-group filters" formArrayName="filters">
|
<ul class="mt-2 list-group filters" formArrayName="filters">
|
||||||
@@ -430,31 +430,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@case (WorkflowActionType.PasswordRemoval) {
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<p class="small" i18n>
|
|
||||||
One password per line. The workflow will try them in order until one succeeds.
|
|
||||||
</p>
|
|
||||||
<pngx-input-textarea
|
|
||||||
i18n-title
|
|
||||||
title="Passwords"
|
|
||||||
formControlName="passwords"
|
|
||||||
rows="4"
|
|
||||||
[error]="error?.actions?.[i]?.passwords"
|
|
||||||
hint="Passwords are stored in plain text. Use with caution."
|
|
||||||
i18n-hint
|
|
||||||
></pngx-input-textarea>
|
|
||||||
</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>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import {
|
import {
|
||||||
FormArray,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -995,32 +994,4 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
component.removeSelectedCustomField(3, formGroup)
|
component.removeSelectedCustomField(3, formGroup)
|
||||||
expect(formGroup.get('assign_custom_fields').value).toEqual([])
|
expect(formGroup.get('assign_custom_fields').value).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle parsing of passwords from array to string and back on save', () => {
|
|
||||||
const passwordAction: WorkflowAction = {
|
|
||||||
id: 1,
|
|
||||||
type: WorkflowActionType.PasswordRemoval,
|
|
||||||
passwords: ['pass1', 'pass2'],
|
|
||||||
}
|
|
||||||
component.object = {
|
|
||||||
name: 'Workflow with Passwords',
|
|
||||||
id: 1,
|
|
||||||
order: 1,
|
|
||||||
enabled: true,
|
|
||||||
triggers: [],
|
|
||||||
actions: [passwordAction],
|
|
||||||
}
|
|
||||||
component.ngOnInit()
|
|
||||||
|
|
||||||
const formActions = component.objectForm.get('actions') as FormArray
|
|
||||||
expect(formActions.value[0].passwords).toBe('pass1\npass2')
|
|
||||||
formActions.at(0).get('passwords').setValue('pass1\npass2\npass3')
|
|
||||||
component.save()
|
|
||||||
|
|
||||||
expect(component.objectForm.get('actions').value[0].passwords).toEqual([
|
|
||||||
'pass1',
|
|
||||||
'pass2',
|
|
||||||
'pass3',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -139,14 +139,6 @@ export const WORKFLOW_ACTION_OPTIONS = [
|
|||||||
id: WorkflowActionType.Webhook,
|
id: WorkflowActionType.Webhook,
|
||||||
name: $localize`Webhook`,
|
name: $localize`Webhook`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: WorkflowActionType.PasswordRemoval,
|
|
||||||
name: $localize`Password removal`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: WorkflowActionType.MoveToTrash,
|
|
||||||
name: $localize`Move to trash`,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export enum TriggerFilterType {
|
export enum TriggerFilterType {
|
||||||
@@ -1210,25 +1202,11 @@ export class WorkflowEditDialogComponent
|
|||||||
headers: new FormControl(action.webhook?.headers),
|
headers: new FormControl(action.webhook?.headers),
|
||||||
include_document: new FormControl(!!action.webhook?.include_document),
|
include_document: new FormControl(!!action.webhook?.include_document),
|
||||||
}),
|
}),
|
||||||
passwords: new FormControl(
|
|
||||||
this.formatPasswords(action.passwords ?? [])
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
{ emitEvent }
|
{ emitEvent }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatPasswords(passwords: string[] = []): string {
|
|
||||||
return passwords.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
private parsePasswords(value: string = ''): string[] {
|
|
||||||
return value
|
|
||||||
.split(/[\n,]+/)
|
|
||||||
.map((entry) => entry.trim())
|
|
||||||
.filter((entry) => entry.length > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateAllTriggerActionFields(emitEvent: boolean = false) {
|
private updateAllTriggerActionFields(emitEvent: boolean = false) {
|
||||||
this.triggerFields.clear({ emitEvent: false })
|
this.triggerFields.clear({ emitEvent: false })
|
||||||
this.object?.triggers.forEach((trigger) => {
|
this.object?.triggers.forEach((trigger) => {
|
||||||
@@ -1353,7 +1331,6 @@ export class WorkflowEditDialogComponent
|
|||||||
headers: null,
|
headers: null,
|
||||||
include_document: false,
|
include_document: false,
|
||||||
},
|
},
|
||||||
passwords: [],
|
|
||||||
}
|
}
|
||||||
this.object.actions.push(action)
|
this.object.actions.push(action)
|
||||||
this.createActionField(action)
|
this.createActionField(action)
|
||||||
@@ -1390,7 +1367,6 @@ export class WorkflowEditDialogComponent
|
|||||||
if (action.type !== WorkflowActionType.Email) {
|
if (action.type !== WorkflowActionType.Email) {
|
||||||
action.email = null
|
action.email = null
|
||||||
}
|
}
|
||||||
action.passwords = this.parsePasswords(action.passwords as any)
|
|
||||||
})
|
})
|
||||||
super.save()
|
super.save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)" [popperOptions]="popperOptions">
|
<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">
|
<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"> {{title}}</div>
|
||||||
@if (!editing && selectionModel.totalCount > 0) {
|
@if (!editing && selectionModel.totalCount > 0) {
|
||||||
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
|
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<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> <ng-container i18n>Remove</ng-container>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
@if (removable) {
|
@if (removable) {
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
<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> <ng-container i18n>Remove</ng-container>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user