Compare commits

..
Author SHA1 Message Date
stumpylog 7ed1b08220 Chore: convert test_signals.py from TestCase to pytest style
22 near-duplicate test methods. Converts to plain pytest classes to enable
pytest.mark.parametrize, which collapses those into 9 test functions covering 23 cases,
with mocker/caplog/settings fixtures replacing unittest.mock/assertLogs/override_settings.

The role-sync scenarios (superuser/staff group sync, in various combinations)
share the same setup and assertions, so they're merged into one parametrize
table.
2026-09-23 14:22:42 -07:00
153 changed files with 33286 additions and 29999 deletions
-53
View File
@@ -1,53 +0,0 @@
name: Install apt packages
description: >
Installs apt packages with a per-attempt timeout and retries. The hosted runner's Azure apt mirror intermittently serves archives at a crawl, which apt's own timeout does not catch. A stuck attempt is killed and retried, which usually lands on a healthy mirror node.
inputs:
packages:
description: Space separated list of packages to install
required: true
attempts:
description: Maximum number of attempts
default: "3"
attempt-timeout:
description: Seconds before a single attempt is killed
default: "180"
runs:
using: composite
steps:
- name: Install ${{ inputs.packages }}
shell: bash
env:
PACKAGES: ${{ inputs.packages }}
ATTEMPTS: ${{ inputs.attempts }}
ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }}
run: |
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
attempt_install() {
$SUDO apt-get update -q
# Intentionally unquoted, this is a list of package names
$SUDO apt-get install -q --yes --no-install-recommends $PACKAGES
}
export -f attempt_install
export SUDO PACKAGES
for attempt in $(seq 1 "${ATTEMPTS}"); do
echo "::group::apt install attempt ${attempt}/${ATTEMPTS}"
start=$(date +%s)
if timeout --kill-after=10 "${ATTEMPT_TIMEOUT}" bash -c attempt_install; then
echo "::endgroup::"
echo "Installed in $(( $(date +%s) - start ))s"
exit 0
fi
echo "::endgroup::"
echo "::warning::apt install attempt ${attempt}/${ATTEMPTS} failed or exceeded ${ATTEMPT_TIMEOUT}s"
# A killed attempt can leave dpkg half configured
$SUDO dpkg --configure -a || true
done
echo "::error::apt install failed after ${ATTEMPTS} attempts"
exit 1
+1
View File
@@ -59,6 +59,7 @@ updates:
- "drf-*" - "drf-*"
- "djangorestframework" - "djangorestframework"
- "whitenoise" - "whitenoise"
- "bleach"
- "jinja2" - "jinja2"
# Async, Task Queuing & Caching # Async, Task Queuing & Caching
async-tasks: async-tasks:
+9 -8
View File
@@ -102,16 +102,17 @@ jobs:
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }} python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install system dependencies - name: Install system dependencies
timeout-minutes: 12 timeout-minutes: 10
uses: $/.github/actions/apt-install run: |
with: sudo apt-get update -qq
packages: unpaper tesseract-ocr imagemagick ghostscript poppler-utils sudo apt-get install -qq --no-install-recommends \
unpaper tesseract-ocr imagemagick ghostscript 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
@@ -138,13 +139,13 @@ jobs:
pytest pytest
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: always() if: always()
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
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@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with: with:
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: coverage.xml files: coverage.xml
@@ -175,7 +176,7 @@ jobs:
with: with:
python-version: "${{ env.DEFAULT_PYTHON }}" python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+3 -3
View File
@@ -106,7 +106,7 @@ jobs:
echo "repository=${repo_name}" echo "repository=${repo_name}"
echo "name=${repo_name}" >> $GITHUB_OUTPUT echo "name=${repo_name}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
@@ -132,7 +132,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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -182,7 +182,7 @@ jobs:
echo "Downloaded digests:" echo "Downloaded digests:"
ls -la /tmp/digests/ ls -la /tmp/digests/
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+6 -7
View File
@@ -174,13 +174,13 @@ 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@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
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@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with: with:
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/ directory: src-ui/coverage/
@@ -216,7 +216,7 @@ jobs:
with: with:
python-version: '3.12' python-version: '3.12'
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: '0.12.x' version: '0.12.x'
enable-cache: false enable-cache: false
@@ -231,10 +231,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: cd src-ui && pnpm install --frozen-lockfile run: cd src-ui && pnpm install --frozen-lockfile
- name: Install backend system dependencies - name: Install backend system dependencies
timeout-minutes: 12 run: |
uses: $/.github/actions/apt-install apt-get update
with: apt-get install --yes --quiet --no-install-recommends libmagic1
packages: libmagic1
- name: Install backend dependencies - name: Install backend dependencies
env: env:
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
+5 -6
View File
@@ -59,7 +59,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
@@ -70,10 +70,9 @@ jobs:
run: | run: |
uv sync --python "${PYTHON_VERSION}" --dev --frozen uv sync --python "${PYTHON_VERSION}" --dev --frozen
- name: Install system dependencies - name: Install system dependencies
timeout-minutes: 12 run: |
uses: $/.github/actions/apt-install sudo apt-get update -qq
with: sudo apt-get install -qq --no-install-recommends gettext liblept5
packages: gettext liblept5
# ---- Build Documentation ---- # ---- Build Documentation ----
- name: Build documentation - name: Build documentation
env: env:
@@ -213,7 +212,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
- name: Run Semgrep - name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning - name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
if: always() if: always()
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0 uses: stumpylog/image-cleaner-action/ephemeral@4fe057d991d63b8f6d5d22c40f17c1bca2226537 # v0.12.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
@@ -56,7 +56,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0 uses: stumpylog/image-cleaner-action/untagged@4fe057d991d63b8f6d5d22c40f17c1bca2226537 # v0.12.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
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.
@@ -47,4 +47,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@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.PNGX_BOT_PAT }} token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false persist-credentials: false
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0 uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3.0.2
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true
+4 -5
View File
@@ -25,12 +25,11 @@ jobs:
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install system dependencies - name: Install system dependencies
timeout-minutes: 12 run: |
uses: $/.github/actions/apt-install sudo apt-get update -qq
with: sudo apt-get install -qq --no-install-recommends gettext
packages: gettext
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+456 -706
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -38,7 +38,7 @@ repos:
- json - json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason # See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.9.8' rev: 'v3.9.6'
hooks: hooks:
- id: prettier - id: prettier
types_or: types_or:
@@ -46,11 +46,11 @@ repos:
- ts - ts
- markdown - markdown
additional_dependencies: additional_dependencies:
- prettier@3.9.8 - prettier@3.9.6
- 'prettier-plugin-organize-imports@4.3.0' - 'prettier-plugin-organize-imports@4.3.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.16.8 rev: v0.16.7
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
+14241 -6104
View File
File diff suppressed because one or more lines are too long
+2 -4
View File
@@ -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.12.20-python3.14-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.12.16-python3.14-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
@@ -171,9 +171,7 @@ RUN set -eux \
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \ && cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& echo "Cleaning up image layer" \ && echo "Cleaning up image layer" \
&& rm --force --verbose *.deb \ && rm --force --verbose *.deb \
&& rm --recursive --force --verbose /var/lib/apt/lists/* \ && rm --recursive --force --verbose /var/lib/apt/lists/*
&& echo "Configuring interactive shells to source the s6 container environment" \
&& echo '. /etc/profile.d/contenv.sh' >> /etc/bash.bashrc
WORKDIR /usr/src/paperless/src/ WORKDIR /usr/src/paperless/src/
+1 -1
View File
@@ -24,7 +24,7 @@ services:
network_mode: host network_mode: host
restart: unless-stopped restart: unless-stopped
greenmail: greenmail:
image: docker.io/greenmail/standalone:2.1.14 image: docker.io/greenmail/standalone:2.1.13
hostname: greenmail hostname: greenmail
container_name: greenmail container_name: greenmail
environment: environment:
-18
View File
@@ -1,18 +0,0 @@
#!/bin/sh
# Source s6 container environment for interactive shells.
# Ensures variables resolved from *_FILE secret injection are visible
# when using 'docker exec bash'. Does not affect s6 services (those
# use with-contenv directly). Has no effect in non-container contexts
# because the directory will not exist.
# Note: sh/dash shells opened via 'docker exec sh' are not covered;
# only bash-based sessions benefit from this file.
_pngx_contenv="/run/s6/container_environment"
if [ -d "${_pngx_contenv}" ]; then
for _pngx_f in "${_pngx_contenv}"/*; do
[ -f "${_pngx_f}" ] || continue
_pngx_name=$(basename "${_pngx_f}")
_pngx_val=$(cat "${_pngx_f}")
export "${_pngx_name}=${_pngx_val}"
done
fi
unset _pngx_contenv _pngx_f _pngx_name _pngx_val
+7 -9
View File
@@ -136,15 +136,13 @@ for suggested generation and embedding models.
### AI-assisted suggestions ### AI-assisted suggestions
With AI enabled, Paperless-ngx can suggest a title, tags, correspondent, document type, With AI enabled, Paperless-ngx can suggest a title, tags, correspondent, document type,
storage path and dates by sending the document to the LLM using "Suggest" button on the document storage path and dates by sending the document to the LLM. This is **opt-in per request**
detail page. You can choose which type of suggestions are requested by default under Settings > and surfaces through the "Suggest" control on the document detail page, alongside the
Documents, either ML (classifier-based) suggestions, AI suggestions, or both. When both are requested classic classifier-based suggestions — it does not disable them. Suggestions are requested
the results are combined. automatically when you open a document that carries an inbox tag unless "Automatically request
suggestions for inbox documents" under Settings > Documents is disabled. Suggestion output
Suggestions are requested automatically when you open a document that carries an inbox tag language can be steered with
unless "Automatically request suggestions for inbox documents" under Settings > Documents is disabled. [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE`](configuration.md#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE)
Suggestion output language can be steered with [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE`](configuration.md#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE)
(otherwise it follows the user's UI language). (otherwise it follows the user's UI language).
### The LLM index (RAG) and similar documents ### The LLM index (RAG) and similar documents
+7 -7
View File
@@ -19,6 +19,7 @@ classifiers = [
dependencies = [ dependencies = [
"azure-ai-documentintelligence>=1.0.2", "azure-ai-documentintelligence>=1.0.2",
"babel>=2.17", "babel>=2.17",
"bleach~=6.4.0",
"celery[redis]~=5.6.2", "celery[redis]~=5.6.2",
"channels~=4.2", "channels~=4.2",
"channels-redis~=4.2", "channels-redis~=4.2",
@@ -34,7 +35,7 @@ dependencies = [
"django-cors-headers~=4.9.0", "django-cors-headers~=4.9.0",
"django-extensions~=4.1", "django-extensions~=4.1",
"django-filter~=25.1", "django-filter~=25.1",
"django-guardian>=3.3.3,<3.6", "django-guardian>=3.3.3,<3.5",
"django-multiselectfield~=1.0.1", "django-multiselectfield~=1.0.1",
"django-rich~=2.2.0", "django-rich~=2.2.0",
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
@@ -44,7 +45,7 @@ dependencies = [
"drf-spectacular-sidecar>=2026.7.1,<2026.10", "drf-spectacular-sidecar>=2026.7.1,<2026.10",
"drf-writable-nested~=0.7.1", "drf-writable-nested~=0.7.1",
"filelock~=3.32.0", "filelock~=3.32.0",
"flower>=2.0.1,<2.3", "flower>=2.0.1,<2.2",
"gotenberg-client[httpx]~=1.0", "gotenberg-client[httpx]~=1.0",
"httpx-oauth~=0.17", "httpx-oauth~=0.17",
"ijson>=3.5.1", "ijson>=3.5.1",
@@ -76,7 +77,6 @@ dependencies = [
"tantivy~=0.26.0", "tantivy~=0.26.0",
"tika-client[httpx]~=1.0", "tika-client[httpx]~=1.0",
"torch>=2.13,<2.15", "torch>=2.13,<2.15",
"turbohtml~=1.10.0",
"watchfiles>=1.2", "watchfiles>=1.2",
"whitenoise~=6.11", "whitenoise~=6.11",
"whoosh-compat[tantivy]==0.3", "whoosh-compat[tantivy]==0.3",
@@ -84,13 +84,13 @@ dependencies = [
] ]
[project.optional-dependencies] [project.optional-dependencies]
mariadb = [ mariadb = [
"mysqlclient>=2.2.7,<2.4", "mysqlclient~=2.2.7",
] ]
postgres = [ postgres = [
"psycopg[c,pool]==3.3.4", "psycopg[c,pool]==3.3.4",
# Direct dependency for proper resolution of the pre-built wheels # Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.3.4", "psycopg-c==3.3.4",
"psycopg-pool==3.3.2", "psycopg-pool==3.3.1",
] ]
webserver = [ webserver = [
"granian[uvloop]>=2.7,<2.9", "granian[uvloop]>=2.7,<2.9",
@@ -112,7 +112,7 @@ lint = [
testing = [ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"faker>=40.36,<40.40", "faker>=40.36,<40.39",
"imagehash", "imagehash",
"pytest~=9.1.1", "pytest~=9.1.1",
"pytest-cov~=7.1.0", "pytest-cov~=7.1.0",
@@ -136,6 +136,7 @@ typing = [
"mypy", "mypy",
"mypy-baseline", "mypy-baseline",
"pyrefly", "pyrefly",
"types-bleach",
"types-channels", "types-channels",
"types-colorama", "types-colorama",
"types-dateparser", "types-dateparser",
@@ -150,7 +151,6 @@ typing = [
[tool.uv] [tool.uv]
required-version = ">=0.9.0" required-version = ">=0.9.0"
prerelease = "disallow"
environments = [ environments = [
"sys_platform == 'darwin'", "sys_platform == 'darwin'",
"sys_platform == 'linux'", "sys_platform == 'linux'",
-5
View File
@@ -91,11 +91,6 @@
"glob": "**/*", "glob": "**/*",
"input": "node_modules/pdfjs-dist/iccs/", "input": "node_modules/pdfjs-dist/iccs/",
"output": "/assets/iccs/" "output": "/assets/iccs/"
},
{
"glob": "**/*",
"input": "node_modules/pdfjs-dist/cmaps/",
"output": "/assets/cmaps/"
} }
], ],
"styles": [ "styles": [
+217 -280
View File
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -15,16 +15,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^22.2.0", "@angular/cdk": "^22.1.6",
"@angular/common": "~22.2.0", "@angular/common": "~22.1.6",
"@angular/compiler": "~22.2.0", "@angular/compiler": "~22.1.6",
"@angular/core": "~22.2.0", "@angular/core": "~22.1.6",
"@angular/forms": "~22.2.0", "@angular/forms": "~22.1.6",
"@angular/localize": "~22.2.0", "@angular/localize": "~22.1.6",
"@angular/platform-browser": "~22.2.0", "@angular/platform-browser": "~22.1.6",
"@angular/router": "~22.2.0", "@angular/router": "~22.1.6",
"@ng-bootstrap/ng-bootstrap": "^21.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~24.1.2", "@ng-select/ng-select": "~24.1.1",
"@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",
@@ -45,29 +45,29 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "^22.0.1", "@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.2.0", "@angular-devkit/core": "^22.1.8",
"@angular-devkit/schematics": "^22.2.0", "@angular-devkit/schematics": "^22.1.8",
"@angular-eslint/builder": "22.5.0", "@angular-eslint/builder": "22.5.0",
"@angular-eslint/eslint-plugin": "22.5.0", "@angular-eslint/eslint-plugin": "22.5.0",
"@angular-eslint/eslint-plugin-template": "22.5.0", "@angular-eslint/eslint-plugin-template": "22.5.0",
"@angular-eslint/schematics": "22.5.0", "@angular-eslint/schematics": "22.5.0",
"@angular-eslint/template-parser": "22.5.0", "@angular-eslint/template-parser": "22.5.0",
"@angular/build": "22.2.0", "@angular/build": "22.1.8",
"@angular/cli": "22.2.0", "@angular/cli": "22.1.8",
"@angular/compiler-cli": "~22.2.0", "@angular/compiler-cli": "~22.1.6",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.6.2", "@types/node": "^26.5.0",
"@typescript-eslint/eslint-plugin": "^8.70.1", "@typescript-eslint/eslint-plugin": "^8.70.0",
"@typescript-eslint/parser": "^8.70.1", "@typescript-eslint/parser": "^8.70.0",
"@typescript-eslint/utils": "^8.70.1", "@typescript-eslint/utils": "^8.70.0",
"eslint": "^10.11.0", "eslint": "^10.10.0",
"jest": "30.5.2", "jest": "30.5.1",
"jest-environment-jsdom": "^30.5.2", "jest-environment-jsdom": "^30.5.1",
"jest-junit": "^17.0.0", "jest-junit": "^17.0.0",
"jest-preset-angular": "^17.0.0", "jest-preset-angular": "^17.0.0",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier": "^3.9.9", "prettier": "^3.9.6",
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.2", "ts-node": "~10.9.2",
"typescript": "^6.0.3" "typescript": "^6.0.3"
+1959 -1311
View File
File diff suppressed because it is too large Load Diff
@@ -253,24 +253,6 @@
</div> </div>
</div> </div>
@if (aiEnabled) {
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Suggestions default to</span>
</div>
<div class="col">
<fieldset class="btn-group btn-group-sm">
<input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceML"><i-bs class="me-1" name="cpu"></i-bs><ng-container i18n>ML only</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceAI"><i-bs class="me-1" name="stars"></i-bs><ng-container i18n>AI only</ng-container></label>
</fieldset>
</div>
</div>
}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-check i18n-title title="Automatically request suggestions for inbox documents" i18n-hint hint="If un-checked, suggestions must be requested via the Suggest button." formControlName="documentEditingAutoSuggest"></pngx-input-check> <pngx-input-check i18n-title title="Automatically request suggestions for inbox documents" i18n-hint hint="If un-checked, suggestions must be requested via the Suggest button." formControlName="documentEditingAutoSuggest"></pngx-input-check>
@@ -307,7 +307,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(35) expect(setSpy).toHaveBeenCalledTimes(34)
expect(setSpy).toHaveBeenCalledWith(SETTINGS_KEYS.SIDEBAR_HIDDEN_ITEMS, [ expect(setSpy).toHaveBeenCalledWith(SETTINGS_KEYS.SIDEBAR_HIDDEN_ITEMS, [
HideableSidebarItemID.Workflows, HideableSidebarItemID.Workflows,
]) ])
@@ -44,7 +44,6 @@ import {
HIDEABLE_SIDEBAR_ITEM_IDS, HIDEABLE_SIDEBAR_ITEM_IDS,
HideableSidebarItemID, HideableSidebarItemID,
SETTINGS_KEYS, SETTINGS_KEYS,
SuggestionSource,
} from 'src/app/data/ui-settings' } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -185,7 +184,6 @@ export class SettingsComponent
documentEditingRemoveInboxTags: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null),
documentEditingOverlayThumbnail: new FormControl(null), documentEditingOverlayThumbnail: new FormControl(null),
documentEditingAutoSuggest: new FormControl(null), documentEditingAutoSuggest: new FormControl(null),
documentEditingSuggestionSource: new FormControl(null),
documentDetailsHiddenFields: new FormControl([]), documentDetailsHiddenFields: new FormControl([]),
searchDbOnly: new FormControl(null), searchDbOnly: new FormControl(null),
searchLink: new FormControl(null), searchLink: new FormControl(null),
@@ -219,11 +217,6 @@ export class SettingsComponent
public readonly PdfZoomScale = PdfZoomScale public readonly PdfZoomScale = PdfZoomScale
public readonly PdfEditorEditMode = PdfEditorEditMode public readonly PdfEditorEditMode = PdfEditorEditMode
public readonly SuggestionSource = SuggestionSource
get aiEnabled(): boolean {
return this.settings.get(SETTINGS_KEYS.AI_ENABLED)
}
public readonly documentDetailFieldOptions = documentDetailFieldOptions public readonly documentDetailFieldOptions = documentDetailFieldOptions
public readonly sidebarItemOptions = HIDEABLE_SIDEBAR_ITEM_IDS.map((id) => ({ public readonly sidebarItemOptions = HIDEABLE_SIDEBAR_ITEM_IDS.map((id) => ({
@@ -411,9 +404,6 @@ export class SettingsComponent
documentEditingAutoSuggest: this.settings.get( documentEditingAutoSuggest: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST
), ),
documentEditingSuggestionSource: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE
),
documentDetailsHiddenFields: this.settings.get( documentDetailsHiddenFields: this.settings.get(
SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS
), ),
@@ -635,10 +625,6 @@ export class SettingsComponent
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST,
this.settingsForm.value.documentEditingAutoSuggest this.settingsForm.value.documentEditingAutoSuggest
) )
this.settings.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
this.settingsForm.value.documentEditingSuggestionSource
)
this.settings.set( this.settings.set(
SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS,
this.settingsForm.value.documentDetailsHiddenFields this.settingsForm.value.documentDetailsHiddenFields
@@ -154,29 +154,11 @@
& section { & section {
position: absolute; position: absolute;
text-align: initial; text-align: initial;
pointer-events: auto;
box-sizing: border-box; box-sizing: border-box;
transform-origin: 0 0; transform-origin: 0 0;
} }
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
position: absolute;
inset: 0;
font-size: 1em;
transition: none;
}
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
> a:hover {
opacity: 0.2;
background-color: rgb(255 255 0);
}
& .annotationTextContent { & .annotationTextContent {
opacity: 0; opacity: 0;
} }
} }
:host ::ng-deep .textLayer.selecting ~ .annotationLayer section {
pointer-events: none;
}
@@ -1,11 +1,7 @@
import { SimpleChange } from '@angular/core' import { SimpleChange } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs' import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
import { import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
LinkTarget,
PDFSinglePageViewer,
PDFViewer,
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import { PngxPdfViewerComponent } from './pdf-viewer.component' import { PngxPdfViewerComponent } from './pdf-viewer.component'
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types' import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
@@ -62,16 +58,6 @@ describe('PngxPdfViewerComponent', () => {
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer) expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
}) })
it('opens external links in a new tab', () => {
const linkService = (component as any).linkService
expect(linkService.options).toEqual(
expect.objectContaining({
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener noreferrer nofollow',
})
)
})
it('resolves the worker source relative to the document base URI', async () => { it('resolves the worker source relative to the document base URI', async () => {
setBaseHref('/paperless/') setBaseHref('/paperless/')
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument') const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
@@ -90,7 +76,6 @@ describe('PngxPdfViewerComponent', () => {
withCredentials: true, withCredentials: true,
wasmUrl: expect.stringContaining('/paperless/assets/wasm/'), wasmUrl: expect.stringContaining('/paperless/assets/wasm/'),
iccUrl: expect.stringContaining('/paperless/assets/iccs/'), iccUrl: expect.stringContaining('/paperless/assets/iccs/'),
cMapUrl: expect.stringContaining('/paperless/assets/cmaps/'),
}) })
}) })
@@ -21,7 +21,6 @@ import {
} from 'pdfjs-dist/legacy/build/pdf.mjs' } from 'pdfjs-dist/legacy/build/pdf.mjs'
import { import {
EventBus, EventBus,
LinkTarget,
PDFFindController, PDFFindController,
PDFLinkService, PDFLinkService,
PDFSinglePageViewer, PDFSinglePageViewer,
@@ -76,11 +75,7 @@ export class PngxPdfViewerComponent
private lastViewerPage?: number private lastViewerPage?: number
private readonly eventBus = new EventBus() private readonly eventBus = new EventBus()
private readonly linkService = new PDFLinkService({ private readonly linkService = new PDFLinkService({ eventBus: this.eventBus })
eventBus: this.eventBus,
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener noreferrer nofollow',
})
private readonly findController = new PDFFindController({ private readonly findController = new PDFFindController({
eventBus: this.eventBus, eventBus: this.eventBus,
linkService: this.linkService, linkService: this.linkService,
@@ -190,7 +185,6 @@ export class PngxPdfViewerComponent
withCredentials: true, withCredentials: true,
wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(), wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(),
iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(), iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(),
cMapUrl: new URL('assets/cmaps/', this.document.baseURI).toString(),
} }
this.loadingTask = getDocument(initOptions) this.loadingTask = getDocument(initOptions)
try { try {
@@ -1,84 +1,58 @@
<div class="d-flex align-items-center"> <div class="btn-group">
<div class="btn-group"> <button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())" [aria-label]="noSuggestions ? 'No suggestions' : 'Suggest'" i18n-aria-label>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())" [aria-label]="noSuggestions ? 'No suggestions' : 'Suggest'" i18n-aria-label> @if (loading()) {
@if (loading()) { <div class="spinner-border spinner-border-sm" role="status"></div>
<div class="spinner-border spinner-border-sm" role="status"></div> } @else if (noSuggestions) {
} @else if (noSuggestions) { <i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs>
<i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs> } @else {
} @else { <i-bs width="1.2em" height="1.2em" name="stars"></i-bs>
<i-bs width="1.2em" height="1.2em" name="lightbulb"></i-bs>
}
@if (noSuggestions) {
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
} @else {
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
}
@if (totalSuggestions > 0) {
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
}
</button>
@if (aiEnabled()) {
<div class="btn-group" ngbDropdown #dropdown="ngbDropdown" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-outline-primary" ngbDropdownToggle [disabled]="disabled() || loading() || !suggestions()" aria-expanded="false" aria-controls="suggestionsDropdown" aria-label="Suggestions dropdown">
<span class="visually-hidden" i18n>Show suggestions</span>
</button>
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
<div class="list-group list-group-flush small pb-0">
@if (novelSuggestions === 0 && reusableSuggestions === 0) {
<div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
</div>
}
@if (suggestions()?.suggested_tags?.length > 0) {
<small class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="tags"></i-bs><ng-container i18n>Tags</ng-container></small>
@for (tag of suggestions().suggested_tags; track tag) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)">{{ tag }}</button>
}
}
@if (suggestions()?.suggested_document_types?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="hash"></i-bs><ng-container i18n>Document Types</ng-container></div>
@for (type of suggestions().suggested_document_types; track type) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)">{{ type }}</button>
}
}
@if (suggestions()?.suggested_correspondents?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="person"></i-bs><ng-container i18n>Correspondents</ng-container></div>
@for (correspondent of suggestions().suggested_correspondents; track correspondent) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
}
}
@if (reusableSuggestions > 0) {
<div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>{reusableSuggestions, plural, =1 {1 existing value suggested below} other {{{reusableSuggestions}} existing values suggested below}}</small>
</div>
}
</div>
</div>
</div>
} }
</div> @if (noSuggestions) {
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
} @else {
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
}
@if (totalSuggestions > 0) {
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
}
</button>
@if (aiEnabled()) { @if (aiEnabled()) {
<div ngbDropdown autoClose="outside" placement="bottom-end" [popperOptions]="popperOptions"> <div class="btn-group" ngbDropdown #dropdown="ngbDropdown" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-link position-relative" ngbDropdownToggle [disabled]="disabled() || loading()" i18n-title title="Suggestion options"> <button type="button" class="btn btn-sm btn-outline-primary" ngbDropdownToggle [disabled]="disabled() || loading() || !suggestions()" aria-expanded="false" aria-controls="suggestionsDropdown" aria-label="Suggestions dropdown">
<i-bs name="three-dots"></i-bs> <span class="visually-hidden" i18n>Show suggestions</span>
@if (source() !== defaultSource()) {
<span class="position-absolute top-0 start-100 translate-middle p-1 bg-primary border border-light rounded-circle">
<span class="visually-hidden" i18n>Not using default</span>
</span>
}
</button> </button>
<div ngbDropdownMenu class="shadow p-3">
<div class="small text-muted mb-2" i18n>Suggest using:</div> <div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
<div class="form-check small"> <div class="list-group list-group-flush small pb-0">
<input class="form-check-input" type="checkbox" id="suggestionSourceML" [checked]="useML" [disabled]="useML && !useAI" (change)="setSources($event.target.checked, useAI)"> @if (novelSuggestions === 0 && reusableSuggestions === 0) {
<label class="form-check-label d-inline-flex align-items-center gap-1" for="suggestionSourceML"><i-bs name="cpu"></i-bs><ng-container i18n>ML</ng-container></label> <div class="list-group-item text-muted fst-italic">
</div> <small class="text-muted small fst-italic" i18n>No novel suggestions</small>
<div class="form-check small"> </div>
<input class="form-check-input" type="checkbox" id="suggestionSourceAI" [checked]="useAI" [disabled]="useAI && !useML" (change)="setSources(useML, $event.target.checked)"> }
<label class="form-check-label d-inline-flex align-items-center gap-1" for="suggestionSourceAI"><i-bs name="stars"></i-bs><ng-container i18n>AI</ng-container></label> @if (suggestions()?.suggested_tags?.length > 0) {
<small class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="tags"></i-bs><ng-container i18n>Tags</ng-container></small>
@for (tag of suggestions().suggested_tags; track tag) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)">{{ tag }}</button>
}
}
@if (suggestions()?.suggested_document_types?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="hash"></i-bs><ng-container i18n>Document Types</ng-container></div>
@for (type of suggestions().suggested_document_types; track type) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)">{{ type }}</button>
}
}
@if (suggestions()?.suggested_correspondents?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="person"></i-bs><ng-container i18n>Correspondents</ng-container></div>
@for (correspondent of suggestions().suggested_correspondents; track correspondent) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
}
}
@if (reusableSuggestions > 0) {
<div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>{reusableSuggestions, plural, =1 {1 existing value suggested below} other {{{reusableSuggestions}} existing values suggested below}}</small>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -1,7 +1,3 @@
.suggestions-dropdown { .suggestions-dropdown {
min-width: 250px; min-width: 250px;
} }
.btn-link.dropdown-toggle::after {
display: none;
}
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SuggestionSource } from 'src/app/data/ui-settings'
import { SuggestionsDropdownComponent } from './suggestions-dropdown.component' import { SuggestionsDropdownComponent } from './suggestions-dropdown.component'
describe('SuggestionsDropdownComponent', () => { describe('SuggestionsDropdownComponent', () => {
@@ -180,71 +179,14 @@ describe('SuggestionsDropdownComponent', () => {
it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => { it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => {
fixture.componentRef.setInput('aiEnabled', true) fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.detectChanges() fixture.detectChanges()
fixture.componentRef.setInput('suggestions', { fixture.componentRef.setInput('suggestions', {
suggested_correspondents: [], suggested_correspondents: [],
suggested_tags: [], suggested_tags: [],
suggested_document_types: [], suggested_document_types: [],
}) })
fixture.detectChanges()
component.clickSuggest() component.clickSuggest()
expect(component.dropdown.isOpen()).toBeTruthy() expect(component.dropdown.open).toBeTruthy()
expect(fixture.nativeElement.textContent).toContain('No novel suggestions') expect(fixture.nativeElement.textContent).toContain('No novel suggestions')
}) })
it('should fetch unfetched sources and show existing suggestions', () => {
jest.spyOn(component.getSuggestions, 'emit')
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.componentRef.setInput('suggestions', { tags: [1] })
fixture.detectChanges()
component.clickSuggest()
expect(component.getSuggestions.emit).toHaveBeenCalledWith(
SuggestionSource.Both
)
expect(component.dropdown.isOpen()).toBeTruthy()
})
it('should only show source options when AI is enabled', () => {
expect(
fixture.nativeElement.querySelector('#suggestionSourceML')
).toBeNull()
fixture.componentRef.setInput('aiEnabled', true)
fixture.detectChanges()
fixture.nativeElement
.querySelector('button[title="Suggestion options"]')
.click()
fixture.detectChanges()
expect(
fixture.nativeElement.querySelector('#suggestionSourceML')
).not.toBeNull()
})
it('should emit source changes and never allow no source', () => {
const emitSpy = jest.spyOn(component.sourceChange, 'emit')
component.setSources(true, true)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.Both)
component.setSources(true, false)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.ML)
component.setSources(false, true)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.AI)
emitSpy.mockClear()
component.setSources(false, false)
expect(emitSpy).not.toHaveBeenCalled()
})
it('should indicate a non-default source', () => {
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.AI)
fixture.componentRef.setInput('defaultSource', SuggestionSource.AI)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).not.toContain('Not using default')
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Not using default')
})
}) })
@@ -8,7 +8,6 @@ import {
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentSuggestions } from 'src/app/data/document-suggestions' import { DocumentSuggestions } from 'src/app/data/document-suggestions'
import { SuggestionSource } from 'src/app/data/ui-settings'
import { pngxPopperOptions } from 'src/app/utils/popper-options' import { pngxPopperOptions } from 'src/app/utils/popper-options'
@Component({ @Component({
@@ -19,16 +18,12 @@ import { pngxPopperOptions } from 'src/app/utils/popper-options'
}) })
export class SuggestionsDropdownComponent { export class SuggestionsDropdownComponent {
public popperOptions = pngxPopperOptions public popperOptions = pngxPopperOptions
public readonly SuggestionSource = SuggestionSource
@ViewChild('dropdown') dropdown: NgbDropdown @ViewChild('dropdown') dropdown: NgbDropdown
readonly suggestions = input<DocumentSuggestions>(null) readonly suggestions = input<DocumentSuggestions>(null)
readonly aiEnabled = input(false) readonly aiEnabled = input(false)
readonly loading = input(false) readonly loading = input(false)
readonly disabled = input(false) readonly disabled = input(false)
readonly source = input<SuggestionSource>(SuggestionSource.ML)
readonly defaultSource = input<SuggestionSource>(SuggestionSource.ML)
readonly fetchedSources = input<SuggestionSource[]>([])
readonly appliedTags = input<number[]>([]) readonly appliedTags = input<number[]>([])
readonly appliedCorrespondent = input<number>(null) readonly appliedCorrespondent = input<number>(null)
@@ -36,10 +31,8 @@ export class SuggestionsDropdownComponent {
readonly appliedStoragePath = input<number>(null) readonly appliedStoragePath = input<number>(null)
@Output() @Output()
getSuggestions: EventEmitter<SuggestionSource> = new EventEmitter() getSuggestions: EventEmitter<SuggestionsDropdownComponent> =
new EventEmitter()
@Output()
sourceChange: EventEmitter<SuggestionSource> = new EventEmitter()
@Output() @Output()
addTag: EventEmitter<string> = new EventEmitter() addTag: EventEmitter<string> = new EventEmitter()
@@ -60,42 +53,12 @@ export class SuggestionsDropdownComponent {
} }
if (!this.suggestions()) { if (!this.suggestions()) {
this.getSuggestions.emit(this.source()) this.getSuggestions.emit(this)
} else if (this.hasUnfetchedSources) {
// sources changed, fetch the rest and show what we have meanwhile
this.getSuggestions.emit(this.source())
this.dropdown?.open()
} else { } else {
this.dropdown?.toggle() this.dropdown?.toggle()
} }
} }
get useML(): boolean {
return this.source() !== SuggestionSource.AI
}
get useAI(): boolean {
return this.source() !== SuggestionSource.ML
}
get hasUnfetchedSources(): boolean {
const fetched = this.fetchedSources()
return (
(this.useML && !fetched.includes(SuggestionSource.ML)) ||
(this.useAI && !fetched.includes(SuggestionSource.AI))
)
}
public setSources(ml: boolean, ai: boolean) {
if (ml && ai) {
this.sourceChange.emit(SuggestionSource.Both)
} else if (ml) {
this.sourceChange.emit(SuggestionSource.ML)
} else if (ai) {
this.sourceChange.emit(SuggestionSource.AI)
}
}
get novelSuggestions(): number { get novelSuggestions(): number {
return ( return (
(this.suggestions()?.suggested_correspondents?.length ?? 0) + (this.suggestions()?.suggested_correspondents?.length ?? 0) +
@@ -134,15 +134,11 @@
[loading]="suggestionsLoading()" [loading]="suggestionsLoading()"
[suggestions]="suggestions()" [suggestions]="suggestions()"
[aiEnabled]="aiEnabled" [aiEnabled]="aiEnabled"
[source]="suggestionSource"
[defaultSource]="defaultSuggestionSource"
[fetchedSources]="fetchedSuggestionSources()"
[appliedTags]="documentForm.value.tags" [appliedTags]="documentForm.value.tags"
[appliedCorrespondent]="documentForm.value.correspondent" [appliedCorrespondent]="documentForm.value.correspondent"
[appliedDocumentType]="documentForm.value.document_type" [appliedDocumentType]="documentForm.value.document_type"
[appliedStoragePath]="documentForm.value.storage_path" [appliedStoragePath]="documentForm.value.storage_path"
(getSuggestions)="getSuggestions($event)" (getSuggestions)="getSuggestions()"
(sourceChange)="suggestionSourceOverride.set($event)"
(addTag)="createTag($event)" (addTag)="createTag($event)"
(addDocumentType)="createDocumentType($event)" (addDocumentType)="createDocumentType($event)"
(addCorrespondent)="createCorrespondent($event)"> (addCorrespondent)="createCorrespondent($event)">
@@ -43,7 +43,7 @@ import {
} from 'src/app/data/filter-rule-type' } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
@@ -1528,113 +1528,6 @@ describe('DocumentDetailComponent', () => {
expect(component.suggestionsLoading()).toBeFalsy() expect(component.suggestionsLoading()).toBeFalsy()
}) })
it('should get and merge ML and AI suggestions when source is both', () => {
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.Both
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42], dates: ['2024-01-01'] }))
const aiSuggestionsSpy = jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(
of({ title: 'AI title', tags: [42, 43], suggested_tags: ['New'] })
)
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
expect(aiSuggestionsSpy).toHaveBeenCalled()
expect(component.suggestions().title).toEqual('AI title')
expect(component.suggestions().tags).toEqual([42, 43])
expect(component.suggestions().suggested_tags).toEqual(['New'])
expect(component.suggestions().dates).toEqual(['2024-01-01'])
})
it('should only fetch sources not yet fetched for the document', () => {
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, false)
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.ML
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42] }))
const aiSuggestionsSpy = jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(of({ tags: [43] }))
initNormally()
component.getSuggestions()
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).not.toHaveBeenCalled()
component.getSuggestions(SuggestionSource.Both)
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).toHaveBeenCalledTimes(1)
expect(component.suggestions().tags).toEqual([42, 43])
component.getSuggestions(SuggestionSource.Both)
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).toHaveBeenCalledTimes(1)
})
it('should use the per-document source override and reset it on document change', () => {
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, false)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
initNormally()
expect(component.suggestionSource).toEqual(SuggestionSource.AI)
component.suggestionSourceOverride.set(SuggestionSource.ML)
expect(component.suggestionSource).toEqual(SuggestionSource.ML)
jest
.spyOn(documentService, 'get')
.mockReturnValueOnce(of(Object.assign({}, doc)))
;(component as any).loadDocument(doc.id, true)
expect(component.suggestionSourceOverride()).toBeNull()
expect(component.fetchedSuggestionSources()).toEqual([])
})
it('should keep suggestions from one source if the other fails', () => {
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.Both
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const errorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42] }))
jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(throwError(() => new Error('failed')))
initNormally()
expect(errorSpy).toHaveBeenCalled()
expect(component.suggestions().tags).toEqual([42])
expect(component.fetchedSuggestionSources()).toEqual([SuggestionSource.ML])
})
it('should show error if needed for get suggestions', () => { it('should show error if needed for get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const errorSpy = jest.spyOn(toastService, 'showError') const errorSpy = jest.spyOn(toastService, 'showError')
@@ -28,7 +28,7 @@ import {
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DeviceDetectorService } from 'ngx-device-detector' import { DeviceDetectorService } from 'ngx-device-detector'
import { BehaviorSubject, merge, Observable, of, Subject, timer } from 'rxjs' import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
import { import {
catchError, catchError,
debounceTime, debounceTime,
@@ -48,10 +48,7 @@ import { DataType } from 'src/app/data/datatype'
import { Document, DocumentVersionInfo } from 'src/app/data/document' import { Document, DocumentVersionInfo } from 'src/app/data/document'
import { DocumentMetadata } from 'src/app/data/document-metadata' import { DocumentMetadata } from 'src/app/data/document-metadata'
import { DocumentNote } from 'src/app/data/document-note' import { DocumentNote } from 'src/app/data/document-note'
import { import { DocumentSuggestions } from 'src/app/data/document-suggestions'
DocumentSuggestions,
mergeSuggestions,
} from 'src/app/data/document-suggestions'
import { DocumentType } from 'src/app/data/document-type' import { DocumentType } from 'src/app/data/document-type'
import { FilterRule } from 'src/app/data/filter-rule' import { FilterRule } from 'src/app/data/filter-rule'
import { import {
@@ -66,7 +63,7 @@ import {
import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithId } from 'src/app/data/object-with-id'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -243,10 +240,6 @@ export class DocumentDetailComponent
private readonly autoSuggestSetting = this.settings.getSignal<boolean>( private readonly autoSuggestSetting = this.settings.getSignal<boolean>(
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST
) )
private readonly suggestionSourceSetting =
this.settings.getSignal<SuggestionSource>(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE
)
private readonly hiddenFieldsSetting = this.settings.getSignal< private readonly hiddenFieldsSetting = this.settings.getSignal<
DocumentDetailFieldID[] DocumentDetailFieldID[]
>(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS) >(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
@@ -268,9 +261,6 @@ export class DocumentDetailComponent
readonly metadata = signal<DocumentMetadata>(undefined) readonly metadata = signal<DocumentMetadata>(undefined)
readonly suggestions = signal<DocumentSuggestions>(undefined) readonly suggestions = signal<DocumentSuggestions>(undefined)
readonly suggestionsLoading = signal(false) readonly suggestionsLoading = signal(false)
// per-document, resets on navigation
readonly suggestionSourceOverride = signal<SuggestionSource>(null)
readonly fetchedSuggestionSources = signal<SuggestionSource[]>([])
readonly users = signal<User[]>(undefined) readonly users = signal<User[]>(undefined)
readonly title = signal<string>(undefined) readonly title = signal<string>(undefined)
@@ -375,15 +365,6 @@ export class DocumentDetailComponent
return this.autoSuggestSetting() return this.autoSuggestSetting()
} }
get defaultSuggestionSource(): SuggestionSource {
return this.aiEnabled ? this.suggestionSourceSetting() : SuggestionSource.ML
}
get suggestionSource(): SuggestionSource {
if (!this.aiEnabled) return SuggestionSource.ML
return this.suggestionSourceOverride() ?? this.defaultSuggestionSource
}
get archiveContentRenderType(): ContentRenderType { get archiveContentRenderType(): ContentRenderType {
const hasArchiveVersion = const hasArchiveVersion =
this.metadata()?.has_archive_version ?? this.metadata()?.has_archive_version ??
@@ -609,8 +590,6 @@ export class DocumentDetailComponent
} }
this.documentId.set(doc.id) this.documentId.set(doc.id)
this.suggestions.set(null) this.suggestions.set(null)
this.suggestionSourceOverride.set(null)
this.fetchedSuggestionSources.set([])
const openDocument = this.openDocumentService.getOpenDocument( const openDocument = this.openDocumentService.getOpenDocument(
this.documentId() this.documentId()
) )
@@ -1098,44 +1077,29 @@ export class DocumentDetailComponent
return this.documentForm.get('custom_fields') as FormArray return this.documentForm.get('custom_fields') as FormArray
} }
getSuggestions(source: SuggestionSource = this.suggestionSource) { getSuggestions() {
const sources = (
source === SuggestionSource.Both
? [SuggestionSource.ML, SuggestionSource.AI]
: [source]
).filter((s) => !this.fetchedSuggestionSources().includes(s))
if (!sources.length) return
this.suggestionsLoading.set(true) this.suggestionsLoading.set(true)
merge( const suggestionsObservable = this.aiEnabled
...sources.map((s) => ? this.documentsService.getAiSuggestions(this.documentId())
(s === SuggestionSource.AI : this.documentsService.getSuggestions(this.documentId())
? this.documentsService.getAiSuggestions(this.documentId()) suggestionsObservable
: this.documentsService.getSuggestions(this.documentId())
).pipe(
first(),
map((result) => ({ source: s, result })),
catchError((error) => {
this.toastService.showError(
$localize`Error retrieving suggestions.`,
error
)
return of(null)
})
)
)
)
.pipe( .pipe(
first(),
takeUntil(this.unsubscribeNotifier), takeUntil(this.unsubscribeNotifier),
takeUntil(this.docChangeNotifier), takeUntil(this.docChangeNotifier),
finalize(() => this.suggestionsLoading.set(false)) finalize(() => this.suggestionsLoading.set(false))
) )
.subscribe((response) => { .subscribe({
if (!response) return next: (result) => {
this.fetchedSuggestionSources.update((f) => [...f, response.source]) this.suggestions.set(result)
this.suggestions.set( },
mergeSuggestions(this.suggestions(), response.result) error: (error) => {
) this.suggestions.set(null)
this.toastService.showError(
$localize`Error retrieving suggestions.`,
error
)
},
}) })
} }
@@ -15,33 +15,3 @@ export interface DocumentSuggestions {
dates?: string[] // ISO-formatted date string e.g. 2022-11-03 dates?: string[] // ISO-formatted date string e.g. 2022-11-03
} }
const union = <T>(a: T[] = [], b: T[] = []): T[] => [...new Set([...a, ...b])]
export function mergeSuggestions(
a: DocumentSuggestions,
b: DocumentSuggestions
): DocumentSuggestions {
if (!a) return b
return {
title: a.title || b.title,
tags: union(a.tags, b.tags),
suggested_tags: union(a.suggested_tags, b.suggested_tags),
correspondents: union(a.correspondents, b.correspondents),
suggested_correspondents: union(
a.suggested_correspondents,
b.suggested_correspondents
),
document_types: union(a.document_types, b.document_types),
suggested_document_types: union(
a.suggested_document_types,
b.suggested_document_types
),
storage_paths: union(a.storage_paths, b.storage_paths),
suggested_storage_paths: union(
a.suggested_storage_paths,
b.suggested_storage_paths
),
dates: union(a.dates, b.dates),
}
}
-13
View File
@@ -20,12 +20,6 @@ export enum GlobalSearchType {
TITLE_CONTENT = 'title-content', TITLE_CONTENT = 'title-content',
} }
export enum SuggestionSource {
ML = 'ml',
AI = 'ai',
Both = 'both',
}
export enum CollapsibleSection { export enum CollapsibleSection {
ATTRIBUTES = 'attributes', ATTRIBUTES = 'attributes',
} }
@@ -104,8 +98,6 @@ export const SETTINGS_KEYS = {
'general-settings:document-editing:overlay-thumbnail', 'general-settings:document-editing:overlay-thumbnail',
DOCUMENT_EDITING_AUTO_SUGGEST: DOCUMENT_EDITING_AUTO_SUGGEST:
'general-settings:document-editing:auto-suggest', 'general-settings:document-editing:auto-suggest',
DOCUMENT_EDITING_SUGGESTION_SOURCE:
'general-settings:document-editing:suggestion-source',
DOCUMENT_DETAILS_HIDDEN_FIELDS: DOCUMENT_DETAILS_HIDDEN_FIELDS:
'general-settings:document-details:hidden-fields', 'general-settings:document-details:hidden-fields',
SEARCH_DB_ONLY: 'general-settings:search:db-only', SEARCH_DB_ONLY: 'general-settings:search:db-only',
@@ -334,11 +326,6 @@ export const SETTINGS: UiSetting[] = [
type: 'boolean', type: 'boolean',
default: true, default: true,
}, },
{
key: SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
type: 'string',
default: SuggestionSource.AI,
},
{ {
key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS,
type: 'array', type: 'array',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-4
View File
@@ -74,7 +74,6 @@ import {
clipboardCheckFill, clipboardCheckFill,
clipboardFill, clipboardFill,
clockHistory, clockHistory,
cpu,
creditCard, creditCard,
dash, dash,
dashCircle, dashCircle,
@@ -119,7 +118,6 @@ import {
infoCircle, infoCircle,
journalBookmarkFill, journalBookmarkFill,
journals, journals,
lightbulb,
link, link,
list, list,
listNested, listNested,
@@ -324,7 +322,6 @@ const icons = {
clipboardCheckFill, clipboardCheckFill,
clipboardFill, clipboardFill,
clockHistory, clockHistory,
cpu,
cash, cash,
creditCard, creditCard,
dash, dash,
@@ -370,7 +367,6 @@ const icons = {
infoCircle, infoCircle,
journalBookmarkFill, journalBookmarkFill,
journals, journals,
lightbulb,
link, link,
list, list,
listNested, listNested,
-1
View File
@@ -292,7 +292,6 @@ a.btn-link:active,
a.btn-link:focus-visible, a.btn-link:focus-visible,
.btn-link:hover, .btn-link:hover,
.btn-link:active, .btn-link:active,
.btn-link.show,
.btn-link:focus-visible { .btn-link:focus-visible {
color: var(--pngx-primary-lighten-10) !important; color: var(--pngx-primary-lighten-10) !important;
.primary-light & { .primary-light & {
@@ -25,20 +25,10 @@ export class PDFFindController {
onIsPageVisible?: () => boolean onIsPageVisible?: () => boolean
} }
export const LinkTarget = {
NONE: 0,
SELF: 1,
BLANK: 2,
PARENT: 3,
TOP: 4,
}
export class PDFLinkService { export class PDFLinkService {
private document?: unknown private document?: unknown
private viewer?: unknown private viewer?: unknown
constructor(readonly options: Record<string, unknown> = {}) {}
setDocument(document: unknown): void { setDocument(document: unknown): void {
this.document = document this.document = document
} }
+2 -3
View File
@@ -857,9 +857,8 @@ class ConsumerPlugin(
self.log.debug(f"Creation date from parse_date: {create_date}") self.log.debug(f"Creation date from parse_date: {create_date}")
else: else:
stats = Path(self.input_doc.original_file).stat() stats = Path(self.input_doc.original_file).stat()
create_date = datetime.datetime.fromtimestamp( create_date = timezone.make_aware(
stats.st_mtime, datetime.datetime.fromtimestamp(stats.st_mtime),
tz=timezone.get_current_timezone(),
) )
self.log.debug(f"Creation date from st_mtime: {create_date}") self.log.debug(f"Creation date from st_mtime: {create_date}")
-10
View File
@@ -1079,16 +1079,6 @@ class DocumentSerializer(
) )
def get_page_count(self, obj) -> int | None: def get_page_count(self, obj) -> int | None:
# Like content versions get their own page count from the newest version,
# use the prefetched versions cache to avoid an extra query
prefetched_cache = getattr(obj, "_prefetched_objects_cache", None)
prefetched_versions = (
prefetched_cache.get("versions")
if isinstance(prefetched_cache, dict)
else None
)
if obj.root_document_id is None and prefetched_versions:
return sort_versions_newest_first(prefetched_versions)[0].page_count
return obj.page_count return obj.page_count
@extend_schema_field(DuplicateDocumentSummarySerializer(many=True)) @extend_schema_field(DuplicateDocumentSummarySerializer(many=True))
+1 -6
View File
@@ -56,7 +56,6 @@ from documents.permissions import get_objects_for_user_owner_aware
from documents.plugins.helpers import DocumentsStatusManager from documents.plugins.helpers import DocumentsStatusManager
from documents.templating.utils import convert_format_str_to_template_format from documents.templating.utils import convert_format_str_to_template_format
from documents.utils import compute_checksum from documents.utils import compute_checksum
from documents.utils import copy_file_with_basic_stats
from documents.workflows.actions import build_workflow_action_context from documents.workflows.actions import build_workflow_action_context
from documents.workflows.actions import execute_email_action from documents.workflows.actions import execute_email_action
from documents.workflows.actions import execute_move_to_trash_action from documents.workflows.actions import execute_move_to_trash_action
@@ -364,11 +363,7 @@ def cleanup_document_deletion(sender, instance, **kwargs) -> None:
logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}") logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}")
try: try:
shutil.move( shutil.move(instance.source_path, new_file_path)
instance.source_path,
new_file_path,
copy_function=copy_file_with_basic_stats,
)
except OSError as e: except OSError as e:
logger.error( logger.error(
f"Failed to move {instance.source_path} to trash at " f"Failed to move {instance.source_path} to trash at "
+1 -1
View File
@@ -12,7 +12,7 @@
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow">
<meta name="author" content="The Paperless-ngx Team"> <meta name="author" content="The Paperless-ngx Team">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="manifest" href="{% static webmanifest %}" crossorigin="use-credentials"> <link rel="manifest" href="{% static webmanifest %}">
<link rel="stylesheet" href="{% static styles_css %}"> <link rel="stylesheet" href="{% static styles_css %}">
<link rel="apple-touch-icon" href="{% static apple_touch_icon %}"> <link rel="apple-touch-icon" href="{% static apple_touch_icon %}">
</head> </head>
@@ -19,7 +19,6 @@ from documents.models import Document
from documents.versioning import annotate_effective_content from documents.versioning import annotate_effective_content
from documents.views import DocumentSelectionMixin from documents.views import DocumentSelectionMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory from paperless_testing.factories import UserFactory
from paperless_testing.http import read_streaming_response from paperless_testing.http import read_streaming_response
from paperless_testing.permissions import grant_global from paperless_testing.permissions import grant_global
@@ -822,26 +821,6 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.data["content"], "v1-content") self.assertEqual(resp.data["content"], "v1-content")
def test_page_count_resolves_to_latest_version(self) -> None:
root = DocumentFactory(page_count=2)
DocumentFactory(root_document=root, version_index=1, page_count=1)
unversioned = DocumentFactory(page_count=5)
resp = self.client.get("/api/documents/?fields=id,page_count")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(
{doc["id"]: doc["page_count"] for doc in resp.data["results"]},
{root.id: 1, unversioned.id: 5},
)
resp = self.client.get(f"/api/documents/{root.id}/")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.data["page_count"], 1)
resp = self.client.get(f"/api/documents/{root.id}/?version={root.id}")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.data["page_count"], 2)
def _make_root_with_out_of_order_versions(self) -> tuple[Document, ...]: def _make_root_with_out_of_order_versions(self) -> tuple[Document, ...]:
""" """
A root whose newest version has a *lower* id than an older one, which is A root whose newest version has a *lower* id than an older one, which is
+2 -6
View File
@@ -1192,7 +1192,6 @@ class DocumentViewSet(
"version_label", "version_label",
"root_document_id", "root_document_id",
"version_index", "version_index",
"page_count",
), ),
), ),
"tags", "tags",
@@ -1272,16 +1271,13 @@ class DocumentViewSet(
if ( if (
"version" not in request.query_params "version" not in request.query_params
or not isinstance(response.data, dict) or not isinstance(response.data, dict)
or not ({"content", "page_count"} & response.data.keys()) or "content" not in response.data
): ):
return response return response
root_doc = self.get_object() root_doc = self.get_object()
content_doc = self._resolve_file_doc(root_doc, request) content_doc = self._resolve_file_doc(root_doc, request)
if "content" in response.data: response.data["content"] = content_doc.content or ""
response.data["content"] = content_doc.content or ""
if "page_count" in response.data:
response.data["page_count"] = content_doc.page_count
return response return response
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
+67 -71
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-02 23:09+0000\n" "POT-Creation-Date: 2026-09-18 01:29+0000\n"
"PO-Revision-Date: 2026-10-02 23:10\n" "PO-Revision-Date: 2026-09-18 01:30\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
@@ -1615,7 +1615,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:514 documents/serialisers.py:871 #: documents/serialisers.py:514 documents/serialisers.py:871
#: documents/serialisers.py:2895 documents/views.py:342 documents/views.py:2729 #: documents/serialisers.py:2883 documents/views.py:343 documents/views.py:2726
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1624,38 +1624,38 @@ msgstr ""
msgid "Invalid color." msgid "Invalid color."
msgstr "Ongeldige kleur." msgstr "Ongeldige kleur."
#: documents/serialisers.py:2362 #: documents/serialisers.py:2350
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Lêertipe %(type)s word nie ondersteun nie" msgstr "Lêertipe %(type)s word nie ondersteun nie"
#: documents/serialisers.py:2406 #: documents/serialisers.py:2394
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2413 #: documents/serialisers.py:2401
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2430 documents/serialisers.py:2440 #: documents/serialisers.py:2418 documents/serialisers.py:2428
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2435 #: documents/serialisers.py:2423
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2582 #: documents/serialisers.py:2570
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ongeldige veranderlike bespeur." msgstr "Ongeldige veranderlike bespeur."
#: documents/serialisers.py:2951 #: documents/serialisers.py:2939
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2981 documents/views.py:4784 #: documents/serialisers.py:2969 documents/views.py:4763
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1910,40 +1910,40 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:335 documents/views.py:2726 #: documents/views.py:336 documents/views.py:2723
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1673 #: documents/views.py:1670
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1684 #: documents/views.py:1681
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:1696 #: documents/views.py:1693
msgid "AI backend rejected the request. Check logs for details." msgid "AI backend rejected the request. Check logs for details."
msgstr "" msgstr ""
#: documents/views.py:2551 documents/views.py:2867 #: documents/views.py:2548 documents/views.py:2864
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4797 #: documents/views.py:4776
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4843 #: documents/views.py:4822
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4907 #: documents/views.py:4886
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4921 #: documents/views.py:4900
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
@@ -2188,198 +2188,194 @@ msgid "Sets the LLM embedding model"
msgstr "" msgstr ""
#: paperless/models.py:369 #: paperless/models.py:369
msgid "Sets the LLM embedding API key"
msgstr ""
#: paperless/models.py:376
msgid "Sets the LLM embedding endpoint, optional" msgid "Sets the LLM embedding endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:383 #: paperless/models.py:376
msgid "Sets the LLM embedding chunk size" msgid "Sets the LLM embedding chunk size"
msgstr "" msgstr ""
#: paperless/models.py:389 #: paperless/models.py:382
msgid "Sets the LLM context size" msgid "Sets the LLM context size"
msgstr "" msgstr ""
#: paperless/models.py:395 #: paperless/models.py:388
msgid "Sets the LLM backend" msgid "Sets the LLM backend"
msgstr "" msgstr ""
#: paperless/models.py:403 #: paperless/models.py:396
msgid "Sets the LLM model" msgid "Sets the LLM model"
msgstr "" msgstr ""
#: paperless/models.py:410 #: paperless/models.py:403
msgid "Sets the LLM API key" msgid "Sets the LLM API key"
msgstr "" msgstr ""
#: paperless/models.py:417 #: paperless/models.py:410
msgid "Sets the LLM endpoint, optional" msgid "Sets the LLM endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:424 #: paperless/models.py:417
msgid "Sets the LLM output language" msgid "Sets the LLM output language"
msgstr "" msgstr ""
#: paperless/models.py:431 #: paperless/models.py:424
msgid "Sets the LLM timeout in seconds" msgid "Sets the LLM timeout in seconds"
msgstr "" msgstr ""
#: paperless/models.py:437 #: paperless/models.py:430
msgid "paperless application settings" msgid "paperless application settings"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:560 #: paperless/settings/__init__.py:558
msgid "English (US)" msgid "English (US)"
msgstr "Engels (VS)" msgstr "Engels (VS)"
#: paperless/settings/__init__.py:561 #: paperless/settings/__init__.py:559
msgid "Arabic" msgid "Arabic"
msgstr "Arabies" msgstr "Arabies"
#: paperless/settings/__init__.py:562 #: paperless/settings/__init__.py:560
msgid "Afrikaans" msgid "Afrikaans"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:563 #: paperless/settings/__init__.py:561
msgid "Belarusian" msgid "Belarusian"
msgstr "Belorussies" msgstr "Belorussies"
#: paperless/settings/__init__.py:564 #: paperless/settings/__init__.py:562
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:565 #: paperless/settings/__init__.py:563
msgid "Catalan" msgid "Catalan"
msgstr "Katalaans" msgstr "Katalaans"
#: paperless/settings/__init__.py:566 #: paperless/settings/__init__.py:564
msgid "Czech" msgid "Czech"
msgstr "Tsjeggies" msgstr "Tsjeggies"
#: paperless/settings/__init__.py:567 #: paperless/settings/__init__.py:565
msgid "Danish" msgid "Danish"
msgstr "Deens" msgstr "Deens"
#: paperless/settings/__init__.py:568 #: paperless/settings/__init__.py:566
msgid "German" msgid "German"
msgstr "Duits" msgstr "Duits"
#: paperless/settings/__init__.py:569 #: paperless/settings/__init__.py:567
msgid "Greek" msgid "Greek"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:570 #: paperless/settings/__init__.py:568
msgid "English (GB)" msgid "English (GB)"
msgstr "Engels (GB)" msgstr "Engels (GB)"
#: paperless/settings/__init__.py:571 #: paperless/settings/__init__.py:569
msgid "Spanish" msgid "Spanish"
msgstr "Spaans" msgstr "Spaans"
#: paperless/settings/__init__.py:572 #: paperless/settings/__init__.py:570
msgid "Persian" msgid "Persian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:573 #: paperless/settings/__init__.py:571
msgid "Finnish" msgid "Finnish"
msgstr "Fins" msgstr "Fins"
#: paperless/settings/__init__.py:574 #: paperless/settings/__init__.py:572
msgid "French" msgid "French"
msgstr "Frans" msgstr "Frans"
#: paperless/settings/__init__.py:575 #: paperless/settings/__init__.py:573
msgid "Hungarian" msgid "Hungarian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:576 #: paperless/settings/__init__.py:574
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:577 #: paperless/settings/__init__.py:575
msgid "Italian" msgid "Italian"
msgstr "Italiaans" msgstr "Italiaans"
#: paperless/settings/__init__.py:578 #: paperless/settings/__init__.py:576
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:579 #: paperless/settings/__init__.py:577
msgid "Korean" msgid "Korean"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:580 #: paperless/settings/__init__.py:578
msgid "Luxembourgish" msgid "Luxembourgish"
msgstr "Luxemburgs" msgstr "Luxemburgs"
#: paperless/settings/__init__.py:581 #: paperless/settings/__init__.py:579
msgid "Norwegian" msgid "Norwegian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:582 #: paperless/settings/__init__.py:580
msgid "Dutch" msgid "Dutch"
msgstr "Nederlands" msgstr "Nederlands"
#: paperless/settings/__init__.py:583 #: paperless/settings/__init__.py:581
msgid "Polish" msgid "Polish"
msgstr "Pools" msgstr "Pools"
#: paperless/settings/__init__.py:584 #: paperless/settings/__init__.py:582
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "Portugees (Brasilië)" msgstr "Portugees (Brasilië)"
#: paperless/settings/__init__.py:585 #: paperless/settings/__init__.py:583
msgid "Portuguese" msgid "Portuguese"
msgstr "Portugees" msgstr "Portugees"
#: paperless/settings/__init__.py:586 #: paperless/settings/__init__.py:584
msgid "Romanian" msgid "Romanian"
msgstr "Roemeens" msgstr "Roemeens"
#: paperless/settings/__init__.py:587 #: paperless/settings/__init__.py:585
msgid "Russian" msgid "Russian"
msgstr "Russies" msgstr "Russies"
#: paperless/settings/__init__.py:588 #: paperless/settings/__init__.py:586
msgid "Slovak" msgid "Slovak"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:589 #: paperless/settings/__init__.py:587
msgid "Slovenian" msgid "Slovenian"
msgstr "Sloweens" msgstr "Sloweens"
#: paperless/settings/__init__.py:590 #: paperless/settings/__init__.py:588
msgid "Serbian" msgid "Serbian"
msgstr "Serwies" msgstr "Serwies"
#: paperless/settings/__init__.py:591 #: paperless/settings/__init__.py:589
msgid "Swedish" msgid "Swedish"
msgstr "Sweeds" msgstr "Sweeds"
#: paperless/settings/__init__.py:592 #: paperless/settings/__init__.py:590
msgid "Turkish" msgid "Turkish"
msgstr "Turks" msgstr "Turks"
#: paperless/settings/__init__.py:593 #: paperless/settings/__init__.py:591
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:594 #: paperless/settings/__init__.py:592
msgid "Vietnamese" msgid "Vietnamese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:595 #: paperless/settings/__init__.py:593
msgid "Chinese Simplified" msgid "Chinese Simplified"
msgstr "Vereenvoudigde Sjinees" msgstr "Vereenvoudigde Sjinees"
#: paperless/settings/__init__.py:596 #: paperless/settings/__init__.py:594
msgid "Chinese Traditional" msgid "Chinese Traditional"
msgstr "" msgstr ""
#: paperless/urls.py:438 #: paperless/urls.py:435
msgid "Paperless-ngx administration" msgid "Paperless-ngx administration"
msgstr "Paperless-ngx administrasie" msgstr "Paperless-ngx administrasie"
+67 -71
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-02 23:09+0000\n" "POT-Creation-Date: 2026-09-18 01:29+0000\n"
"PO-Revision-Date: 2026-10-02 23:10\n" "PO-Revision-Date: 2026-09-18 01:30\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Amharic\n" "Language-Team: Amharic\n"
"Language: am_ET\n" "Language: am_ET\n"
@@ -1615,7 +1615,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:514 documents/serialisers.py:871 #: documents/serialisers.py:514 documents/serialisers.py:871
#: documents/serialisers.py:2895 documents/views.py:342 documents/views.py:2729 #: documents/serialisers.py:2883 documents/views.py:343 documents/views.py:2726
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1624,38 +1624,38 @@ msgstr ""
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:2362 #: documents/serialisers.py:2350
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:2406 #: documents/serialisers.py:2394
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2413 #: documents/serialisers.py:2401
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2430 documents/serialisers.py:2440 #: documents/serialisers.py:2418 documents/serialisers.py:2428
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2435 #: documents/serialisers.py:2423
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2582 #: documents/serialisers.py:2570
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""
#: documents/serialisers.py:2951 #: documents/serialisers.py:2939
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2981 documents/views.py:4784 #: documents/serialisers.py:2969 documents/views.py:4763
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1910,40 +1910,40 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:335 documents/views.py:2726 #: documents/views.py:336 documents/views.py:2723
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1673 #: documents/views.py:1670
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1684 #: documents/views.py:1681
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:1696 #: documents/views.py:1693
msgid "AI backend rejected the request. Check logs for details." msgid "AI backend rejected the request. Check logs for details."
msgstr "" msgstr ""
#: documents/views.py:2551 documents/views.py:2867 #: documents/views.py:2548 documents/views.py:2864
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4797 #: documents/views.py:4776
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4843 #: documents/views.py:4822
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4907 #: documents/views.py:4886
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4921 #: documents/views.py:4900
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
@@ -2188,198 +2188,194 @@ msgid "Sets the LLM embedding model"
msgstr "" msgstr ""
#: paperless/models.py:369 #: paperless/models.py:369
msgid "Sets the LLM embedding API key"
msgstr ""
#: paperless/models.py:376
msgid "Sets the LLM embedding endpoint, optional" msgid "Sets the LLM embedding endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:383 #: paperless/models.py:376
msgid "Sets the LLM embedding chunk size" msgid "Sets the LLM embedding chunk size"
msgstr "" msgstr ""
#: paperless/models.py:389 #: paperless/models.py:382
msgid "Sets the LLM context size" msgid "Sets the LLM context size"
msgstr "" msgstr ""
#: paperless/models.py:395 #: paperless/models.py:388
msgid "Sets the LLM backend" msgid "Sets the LLM backend"
msgstr "" msgstr ""
#: paperless/models.py:403 #: paperless/models.py:396
msgid "Sets the LLM model" msgid "Sets the LLM model"
msgstr "" msgstr ""
#: paperless/models.py:410 #: paperless/models.py:403
msgid "Sets the LLM API key" msgid "Sets the LLM API key"
msgstr "" msgstr ""
#: paperless/models.py:417 #: paperless/models.py:410
msgid "Sets the LLM endpoint, optional" msgid "Sets the LLM endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:424 #: paperless/models.py:417
msgid "Sets the LLM output language" msgid "Sets the LLM output language"
msgstr "" msgstr ""
#: paperless/models.py:431 #: paperless/models.py:424
msgid "Sets the LLM timeout in seconds" msgid "Sets the LLM timeout in seconds"
msgstr "" msgstr ""
#: paperless/models.py:437 #: paperless/models.py:430
msgid "paperless application settings" msgid "paperless application settings"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:560 #: paperless/settings/__init__.py:558
msgid "English (US)" msgid "English (US)"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:561 #: paperless/settings/__init__.py:559
msgid "Arabic" msgid "Arabic"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:562 #: paperless/settings/__init__.py:560
msgid "Afrikaans" msgid "Afrikaans"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:563 #: paperless/settings/__init__.py:561
msgid "Belarusian" msgid "Belarusian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:564 #: paperless/settings/__init__.py:562
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:565 #: paperless/settings/__init__.py:563
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:566 #: paperless/settings/__init__.py:564
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:567 #: paperless/settings/__init__.py:565
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:568 #: paperless/settings/__init__.py:566
msgid "German" msgid "German"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:569 #: paperless/settings/__init__.py:567
msgid "Greek" msgid "Greek"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:570 #: paperless/settings/__init__.py:568
msgid "English (GB)" msgid "English (GB)"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:571 #: paperless/settings/__init__.py:569
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:572 #: paperless/settings/__init__.py:570
msgid "Persian" msgid "Persian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:573 #: paperless/settings/__init__.py:571
msgid "Finnish" msgid "Finnish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:574 #: paperless/settings/__init__.py:572
msgid "French" msgid "French"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:575 #: paperless/settings/__init__.py:573
msgid "Hungarian" msgid "Hungarian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:576 #: paperless/settings/__init__.py:574
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:577 #: paperless/settings/__init__.py:575
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:578 #: paperless/settings/__init__.py:576
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:579 #: paperless/settings/__init__.py:577
msgid "Korean" msgid "Korean"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:580 #: paperless/settings/__init__.py:578
msgid "Luxembourgish" msgid "Luxembourgish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:581 #: paperless/settings/__init__.py:579
msgid "Norwegian" msgid "Norwegian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:582 #: paperless/settings/__init__.py:580
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:583 #: paperless/settings/__init__.py:581
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:584 #: paperless/settings/__init__.py:582
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:585 #: paperless/settings/__init__.py:583
msgid "Portuguese" msgid "Portuguese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:586 #: paperless/settings/__init__.py:584
msgid "Romanian" msgid "Romanian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:587 #: paperless/settings/__init__.py:585
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:588 #: paperless/settings/__init__.py:586
msgid "Slovak" msgid "Slovak"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:589 #: paperless/settings/__init__.py:587
msgid "Slovenian" msgid "Slovenian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:590 #: paperless/settings/__init__.py:588
msgid "Serbian" msgid "Serbian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:591 #: paperless/settings/__init__.py:589
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:592 #: paperless/settings/__init__.py:590
msgid "Turkish" msgid "Turkish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:593 #: paperless/settings/__init__.py:591
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:594 #: paperless/settings/__init__.py:592
msgid "Vietnamese" msgid "Vietnamese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:595 #: paperless/settings/__init__.py:593
msgid "Chinese Simplified" msgid "Chinese Simplified"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:596 #: paperless/settings/__init__.py:594
msgid "Chinese Traditional" msgid "Chinese Traditional"
msgstr "" msgstr ""
#: paperless/urls.py:438 #: paperless/urls.py:435
msgid "Paperless-ngx administration" msgid "Paperless-ngx administration"
msgstr "" msgstr ""
+67 -71
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-02 23:09+0000\n" "POT-Creation-Date: 2026-09-18 01:29+0000\n"
"PO-Revision-Date: 2026-10-02 23:10\n" "PO-Revision-Date: 2026-09-18 01:30\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
@@ -1615,7 +1615,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:514 documents/serialisers.py:871 #: documents/serialisers.py:514 documents/serialisers.py:871
#: documents/serialisers.py:2895 documents/views.py:342 documents/views.py:2729 #: documents/serialisers.py:2883 documents/views.py:343 documents/views.py:2726
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1624,38 +1624,38 @@ msgstr ""
msgid "Invalid color." msgid "Invalid color."
msgstr "لون خاطئ." msgstr "لون خاطئ."
#: documents/serialisers.py:2362 #: documents/serialisers.py:2350
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "نوع الملف %(type)s غير مدعوم" msgstr "نوع الملف %(type)s غير مدعوم"
#: documents/serialisers.py:2406 #: documents/serialisers.py:2394
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2413 #: documents/serialisers.py:2401
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2430 documents/serialisers.py:2440 #: documents/serialisers.py:2418 documents/serialisers.py:2428
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2435 #: documents/serialisers.py:2423
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2582 #: documents/serialisers.py:2570
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "اكتشاف متغير خاطئ." msgstr "اكتشاف متغير خاطئ."
#: documents/serialisers.py:2951 #: documents/serialisers.py:2939
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2981 documents/views.py:4784 #: documents/serialisers.py:2969 documents/views.py:4763
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1911,40 +1911,40 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:335 documents/views.py:2726 #: documents/views.py:336 documents/views.py:2723
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1673 #: documents/views.py:1670
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1684 #: documents/views.py:1681
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:1696 #: documents/views.py:1693
msgid "AI backend rejected the request. Check logs for details." msgid "AI backend rejected the request. Check logs for details."
msgstr "" msgstr ""
#: documents/views.py:2551 documents/views.py:2867 #: documents/views.py:2548 documents/views.py:2864
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4797 #: documents/views.py:4776
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4843 #: documents/views.py:4822
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4907 #: documents/views.py:4886
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4921 #: documents/views.py:4900
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
@@ -2189,198 +2189,194 @@ msgid "Sets the LLM embedding model"
msgstr "" msgstr ""
#: paperless/models.py:369 #: paperless/models.py:369
msgid "Sets the LLM embedding API key"
msgstr ""
#: paperless/models.py:376
msgid "Sets the LLM embedding endpoint, optional" msgid "Sets the LLM embedding endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:383 #: paperless/models.py:376
msgid "Sets the LLM embedding chunk size" msgid "Sets the LLM embedding chunk size"
msgstr "" msgstr ""
#: paperless/models.py:389 #: paperless/models.py:382
msgid "Sets the LLM context size" msgid "Sets the LLM context size"
msgstr "" msgstr ""
#: paperless/models.py:395 #: paperless/models.py:388
msgid "Sets the LLM backend" msgid "Sets the LLM backend"
msgstr "" msgstr ""
#: paperless/models.py:403 #: paperless/models.py:396
msgid "Sets the LLM model" msgid "Sets the LLM model"
msgstr "" msgstr ""
#: paperless/models.py:410 #: paperless/models.py:403
msgid "Sets the LLM API key" msgid "Sets the LLM API key"
msgstr "" msgstr ""
#: paperless/models.py:417 #: paperless/models.py:410
msgid "Sets the LLM endpoint, optional" msgid "Sets the LLM endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:424 #: paperless/models.py:417
msgid "Sets the LLM output language" msgid "Sets the LLM output language"
msgstr "" msgstr ""
#: paperless/models.py:431 #: paperless/models.py:424
msgid "Sets the LLM timeout in seconds" msgid "Sets the LLM timeout in seconds"
msgstr "" msgstr ""
#: paperless/models.py:437 #: paperless/models.py:430
msgid "paperless application settings" msgid "paperless application settings"
msgstr "إعدادات التطبيق paperless" msgstr "إعدادات التطبيق paperless"
#: paperless/settings/__init__.py:560 #: paperless/settings/__init__.py:558
msgid "English (US)" msgid "English (US)"
msgstr "الإنجليزية (الولايات المتحدة)" msgstr "الإنجليزية (الولايات المتحدة)"
#: paperless/settings/__init__.py:561 #: paperless/settings/__init__.py:559
msgid "Arabic" msgid "Arabic"
msgstr "العربية" msgstr "العربية"
#: paperless/settings/__init__.py:562 #: paperless/settings/__init__.py:560
msgid "Afrikaans" msgid "Afrikaans"
msgstr "اللغة الأفريقانية" msgstr "اللغة الأفريقانية"
#: paperless/settings/__init__.py:563 #: paperless/settings/__init__.py:561
msgid "Belarusian" msgid "Belarusian"
msgstr "البيلاروسية" msgstr "البيلاروسية"
#: paperless/settings/__init__.py:564 #: paperless/settings/__init__.py:562
msgid "Bulgarian" msgid "Bulgarian"
msgstr "البلغارية" msgstr "البلغارية"
#: paperless/settings/__init__.py:565 #: paperless/settings/__init__.py:563
msgid "Catalan" msgid "Catalan"
msgstr "اللغة الكتالونية" msgstr "اللغة الكتالونية"
#: paperless/settings/__init__.py:566 #: paperless/settings/__init__.py:564
msgid "Czech" msgid "Czech"
msgstr "التشيكية" msgstr "التشيكية"
#: paperless/settings/__init__.py:567 #: paperless/settings/__init__.py:565
msgid "Danish" msgid "Danish"
msgstr "الدانماركية" msgstr "الدانماركية"
#: paperless/settings/__init__.py:568 #: paperless/settings/__init__.py:566
msgid "German" msgid "German"
msgstr "الألمانية" msgstr "الألمانية"
#: paperless/settings/__init__.py:569 #: paperless/settings/__init__.py:567
msgid "Greek" msgid "Greek"
msgstr "اليونانية" msgstr "اليونانية"
#: paperless/settings/__init__.py:570 #: paperless/settings/__init__.py:568
msgid "English (GB)" msgid "English (GB)"
msgstr "الإنجليزية (المملكة المتحدة)" msgstr "الإنجليزية (المملكة المتحدة)"
#: paperless/settings/__init__.py:571 #: paperless/settings/__init__.py:569
msgid "Spanish" msgid "Spanish"
msgstr "الإسبانية" msgstr "الإسبانية"
#: paperless/settings/__init__.py:572 #: paperless/settings/__init__.py:570
msgid "Persian" msgid "Persian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:573 #: paperless/settings/__init__.py:571
msgid "Finnish" msgid "Finnish"
msgstr "الفنلندية" msgstr "الفنلندية"
#: paperless/settings/__init__.py:574 #: paperless/settings/__init__.py:572
msgid "French" msgid "French"
msgstr "الفرنسية" msgstr "الفرنسية"
#: paperless/settings/__init__.py:575 #: paperless/settings/__init__.py:573
msgid "Hungarian" msgid "Hungarian"
msgstr "المجرية" msgstr "المجرية"
#: paperless/settings/__init__.py:576 #: paperless/settings/__init__.py:574
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:577 #: paperless/settings/__init__.py:575
msgid "Italian" msgid "Italian"
msgstr "الإيطالية" msgstr "الإيطالية"
#: paperless/settings/__init__.py:578 #: paperless/settings/__init__.py:576
msgid "Japanese" msgid "Japanese"
msgstr "اليابانية" msgstr "اليابانية"
#: paperless/settings/__init__.py:579 #: paperless/settings/__init__.py:577
msgid "Korean" msgid "Korean"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:580 #: paperless/settings/__init__.py:578
msgid "Luxembourgish" msgid "Luxembourgish"
msgstr "اللوكسمبرجية" msgstr "اللوكسمبرجية"
#: paperless/settings/__init__.py:581 #: paperless/settings/__init__.py:579
msgid "Norwegian" msgid "Norwegian"
msgstr "النرويجية" msgstr "النرويجية"
#: paperless/settings/__init__.py:582 #: paperless/settings/__init__.py:580
msgid "Dutch" msgid "Dutch"
msgstr "الهولندية" msgstr "الهولندية"
#: paperless/settings/__init__.py:583 #: paperless/settings/__init__.py:581
msgid "Polish" msgid "Polish"
msgstr "البولندية" msgstr "البولندية"
#: paperless/settings/__init__.py:584 #: paperless/settings/__init__.py:582
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "البرتغالية (البرازيل)" msgstr "البرتغالية (البرازيل)"
#: paperless/settings/__init__.py:585 #: paperless/settings/__init__.py:583
msgid "Portuguese" msgid "Portuguese"
msgstr "البرتغالية" msgstr "البرتغالية"
#: paperless/settings/__init__.py:586 #: paperless/settings/__init__.py:584
msgid "Romanian" msgid "Romanian"
msgstr "الرومانية" msgstr "الرومانية"
#: paperless/settings/__init__.py:587 #: paperless/settings/__init__.py:585
msgid "Russian" msgid "Russian"
msgstr "الروسية" msgstr "الروسية"
#: paperless/settings/__init__.py:588 #: paperless/settings/__init__.py:586
msgid "Slovak" msgid "Slovak"
msgstr "السلوفاكية" msgstr "السلوفاكية"
#: paperless/settings/__init__.py:589 #: paperless/settings/__init__.py:587
msgid "Slovenian" msgid "Slovenian"
msgstr "السلوفانية" msgstr "السلوفانية"
#: paperless/settings/__init__.py:590 #: paperless/settings/__init__.py:588
msgid "Serbian" msgid "Serbian"
msgstr "الصربية" msgstr "الصربية"
#: paperless/settings/__init__.py:591 #: paperless/settings/__init__.py:589
msgid "Swedish" msgid "Swedish"
msgstr "السويدية" msgstr "السويدية"
#: paperless/settings/__init__.py:592 #: paperless/settings/__init__.py:590
msgid "Turkish" msgid "Turkish"
msgstr "التركية" msgstr "التركية"
#: paperless/settings/__init__.py:593 #: paperless/settings/__init__.py:591
msgid "Ukrainian" msgid "Ukrainian"
msgstr "الأوكرانية" msgstr "الأوكرانية"
#: paperless/settings/__init__.py:594 #: paperless/settings/__init__.py:592
msgid "Vietnamese" msgid "Vietnamese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:595 #: paperless/settings/__init__.py:593
msgid "Chinese Simplified" msgid "Chinese Simplified"
msgstr "الصينية المبسطة" msgstr "الصينية المبسطة"
#: paperless/settings/__init__.py:596 #: paperless/settings/__init__.py:594
msgid "Chinese Traditional" msgid "Chinese Traditional"
msgstr "" msgstr ""
#: paperless/urls.py:438 #: paperless/urls.py:435
msgid "Paperless-ngx administration" msgid "Paperless-ngx administration"
msgstr "Paperless-ngx الإدارة" msgstr "Paperless-ngx الإدارة"
+67 -71
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-02 23:09+0000\n" "POT-Creation-Date: 2026-09-18 01:29+0000\n"
"PO-Revision-Date: 2026-10-02 23:10\n" "PO-Revision-Date: 2026-09-18 01:30\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Belarusian\n" "Language-Team: Belarusian\n"
"Language: be_BY\n" "Language: be_BY\n"
@@ -1615,7 +1615,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:514 documents/serialisers.py:871 #: documents/serialisers.py:514 documents/serialisers.py:871
#: documents/serialisers.py:2895 documents/views.py:342 documents/views.py:2729 #: documents/serialisers.py:2883 documents/views.py:343 documents/views.py:2726
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1624,38 +1624,38 @@ msgstr ""
msgid "Invalid color." msgid "Invalid color."
msgstr "Няправільны колер." msgstr "Няправільны колер."
#: documents/serialisers.py:2362 #: documents/serialisers.py:2350
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Тып файла %(type)s не падтрымліваецца" msgstr "Тып файла %(type)s не падтрымліваецца"
#: documents/serialisers.py:2406 #: documents/serialisers.py:2394
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2413 #: documents/serialisers.py:2401
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2430 documents/serialisers.py:2440 #: documents/serialisers.py:2418 documents/serialisers.py:2428
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2435 #: documents/serialisers.py:2423
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2582 #: documents/serialisers.py:2570
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Выяўлена няправільная зменная." msgstr "Выяўлена няправільная зменная."
#: documents/serialisers.py:2951 #: documents/serialisers.py:2939
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2981 documents/views.py:4784 #: documents/serialisers.py:2969 documents/views.py:4763
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1910,40 +1910,40 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:335 documents/views.py:2726 #: documents/views.py:336 documents/views.py:2723
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1673 #: documents/views.py:1670
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1684 #: documents/views.py:1681
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:1696 #: documents/views.py:1693
msgid "AI backend rejected the request. Check logs for details." msgid "AI backend rejected the request. Check logs for details."
msgstr "" msgstr ""
#: documents/views.py:2551 documents/views.py:2867 #: documents/views.py:2548 documents/views.py:2864
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4797 #: documents/views.py:4776
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4843 #: documents/views.py:4822
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4907 #: documents/views.py:4886
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4921 #: documents/views.py:4900
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
@@ -2188,198 +2188,194 @@ msgid "Sets the LLM embedding model"
msgstr "" msgstr ""
#: paperless/models.py:369 #: paperless/models.py:369
msgid "Sets the LLM embedding API key"
msgstr ""
#: paperless/models.py:376
msgid "Sets the LLM embedding endpoint, optional" msgid "Sets the LLM embedding endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:383 #: paperless/models.py:376
msgid "Sets the LLM embedding chunk size" msgid "Sets the LLM embedding chunk size"
msgstr "" msgstr ""
#: paperless/models.py:389 #: paperless/models.py:382
msgid "Sets the LLM context size" msgid "Sets the LLM context size"
msgstr "" msgstr ""
#: paperless/models.py:395 #: paperless/models.py:388
msgid "Sets the LLM backend" msgid "Sets the LLM backend"
msgstr "" msgstr ""
#: paperless/models.py:403 #: paperless/models.py:396
msgid "Sets the LLM model" msgid "Sets the LLM model"
msgstr "" msgstr ""
#: paperless/models.py:410 #: paperless/models.py:403
msgid "Sets the LLM API key" msgid "Sets the LLM API key"
msgstr "" msgstr ""
#: paperless/models.py:417 #: paperless/models.py:410
msgid "Sets the LLM endpoint, optional" msgid "Sets the LLM endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:424 #: paperless/models.py:417
msgid "Sets the LLM output language" msgid "Sets the LLM output language"
msgstr "" msgstr ""
#: paperless/models.py:431 #: paperless/models.py:424
msgid "Sets the LLM timeout in seconds" msgid "Sets the LLM timeout in seconds"
msgstr "" msgstr ""
#: paperless/models.py:437 #: paperless/models.py:430
msgid "paperless application settings" msgid "paperless application settings"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:560 #: paperless/settings/__init__.py:558
msgid "English (US)" msgid "English (US)"
msgstr "Англійская (ЗША)" msgstr "Англійская (ЗША)"
#: paperless/settings/__init__.py:561 #: paperless/settings/__init__.py:559
msgid "Arabic" msgid "Arabic"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:562 #: paperless/settings/__init__.py:560
msgid "Afrikaans" msgid "Afrikaans"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:563 #: paperless/settings/__init__.py:561
msgid "Belarusian" msgid "Belarusian"
msgstr "Беларуская" msgstr "Беларуская"
#: paperless/settings/__init__.py:564 #: paperless/settings/__init__.py:562
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:565 #: paperless/settings/__init__.py:563
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:566 #: paperless/settings/__init__.py:564
msgid "Czech" msgid "Czech"
msgstr "Чэшская" msgstr "Чэшская"
#: paperless/settings/__init__.py:567 #: paperless/settings/__init__.py:565
msgid "Danish" msgid "Danish"
msgstr "Дацкая" msgstr "Дацкая"
#: paperless/settings/__init__.py:568 #: paperless/settings/__init__.py:566
msgid "German" msgid "German"
msgstr "Нямецкая" msgstr "Нямецкая"
#: paperless/settings/__init__.py:569 #: paperless/settings/__init__.py:567
msgid "Greek" msgid "Greek"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:570 #: paperless/settings/__init__.py:568
msgid "English (GB)" msgid "English (GB)"
msgstr "Англійская (Вялікабрытанія)" msgstr "Англійская (Вялікабрытанія)"
#: paperless/settings/__init__.py:571 #: paperless/settings/__init__.py:569
msgid "Spanish" msgid "Spanish"
msgstr "Іспанская" msgstr "Іспанская"
#: paperless/settings/__init__.py:572 #: paperless/settings/__init__.py:570
msgid "Persian" msgid "Persian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:573 #: paperless/settings/__init__.py:571
msgid "Finnish" msgid "Finnish"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:574 #: paperless/settings/__init__.py:572
msgid "French" msgid "French"
msgstr "Французская" msgstr "Французская"
#: paperless/settings/__init__.py:575 #: paperless/settings/__init__.py:573
msgid "Hungarian" msgid "Hungarian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:576 #: paperless/settings/__init__.py:574
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:577 #: paperless/settings/__init__.py:575
msgid "Italian" msgid "Italian"
msgstr "Італьянская" msgstr "Італьянская"
#: paperless/settings/__init__.py:578 #: paperless/settings/__init__.py:576
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:579 #: paperless/settings/__init__.py:577
msgid "Korean" msgid "Korean"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:580 #: paperless/settings/__init__.py:578
msgid "Luxembourgish" msgid "Luxembourgish"
msgstr "Люксембургская" msgstr "Люксембургская"
#: paperless/settings/__init__.py:581 #: paperless/settings/__init__.py:579
msgid "Norwegian" msgid "Norwegian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:582 #: paperless/settings/__init__.py:580
msgid "Dutch" msgid "Dutch"
msgstr "Нідэрландская" msgstr "Нідэрландская"
#: paperless/settings/__init__.py:583 #: paperless/settings/__init__.py:581
msgid "Polish" msgid "Polish"
msgstr "Польская" msgstr "Польская"
#: paperless/settings/__init__.py:584 #: paperless/settings/__init__.py:582
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "Партугальская (Бразілія)" msgstr "Партугальская (Бразілія)"
#: paperless/settings/__init__.py:585 #: paperless/settings/__init__.py:583
msgid "Portuguese" msgid "Portuguese"
msgstr "Партугальская" msgstr "Партугальская"
#: paperless/settings/__init__.py:586 #: paperless/settings/__init__.py:584
msgid "Romanian" msgid "Romanian"
msgstr "Румынская" msgstr "Румынская"
#: paperless/settings/__init__.py:587 #: paperless/settings/__init__.py:585
msgid "Russian" msgid "Russian"
msgstr "Руская" msgstr "Руская"
#: paperless/settings/__init__.py:588 #: paperless/settings/__init__.py:586
msgid "Slovak" msgid "Slovak"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:589 #: paperless/settings/__init__.py:587
msgid "Slovenian" msgid "Slovenian"
msgstr "Славенская" msgstr "Славенская"
#: paperless/settings/__init__.py:590 #: paperless/settings/__init__.py:588
msgid "Serbian" msgid "Serbian"
msgstr "Сербская" msgstr "Сербская"
#: paperless/settings/__init__.py:591 #: paperless/settings/__init__.py:589
msgid "Swedish" msgid "Swedish"
msgstr "Шведская" msgstr "Шведская"
#: paperless/settings/__init__.py:592 #: paperless/settings/__init__.py:590
msgid "Turkish" msgid "Turkish"
msgstr "Турэцкая" msgstr "Турэцкая"
#: paperless/settings/__init__.py:593 #: paperless/settings/__init__.py:591
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:594 #: paperless/settings/__init__.py:592
msgid "Vietnamese" msgid "Vietnamese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:595 #: paperless/settings/__init__.py:593
msgid "Chinese Simplified" msgid "Chinese Simplified"
msgstr "Кітайская спрошчаная" msgstr "Кітайская спрошчаная"
#: paperless/settings/__init__.py:596 #: paperless/settings/__init__.py:594
msgid "Chinese Traditional" msgid "Chinese Traditional"
msgstr "" msgstr ""
#: paperless/urls.py:438 #: paperless/urls.py:435
msgid "Paperless-ngx administration" msgid "Paperless-ngx administration"
msgstr "Адміністраванне Paperless-ngx" msgstr "Адміністраванне Paperless-ngx"
+67 -71
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-02 23:09+0000\n" "POT-Creation-Date: 2026-09-18 01:29+0000\n"
"PO-Revision-Date: 2026-10-02 23:10\n" "PO-Revision-Date: 2026-09-18 01:30\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
@@ -1615,7 +1615,7 @@ msgid "workflow runs"
msgstr "стартиране на работните процеси" msgstr "стартиране на работните процеси"
#: documents/serialisers.py:514 documents/serialisers.py:871 #: documents/serialisers.py:514 documents/serialisers.py:871
#: documents/serialisers.py:2895 documents/views.py:342 documents/views.py:2729 #: documents/serialisers.py:2883 documents/views.py:343 documents/views.py:2726
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1624,38 +1624,38 @@ msgstr ""
msgid "Invalid color." msgid "Invalid color."
msgstr "Невалиден цвят." msgstr "Невалиден цвят."
#: documents/serialisers.py:2362 #: documents/serialisers.py:2350
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Файловия тип %(type)s не се поддържа" msgstr "Файловия тип %(type)s не се поддържа"
#: documents/serialisers.py:2406 #: documents/serialisers.py:2394
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2413 #: documents/serialisers.py:2401
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2430 documents/serialisers.py:2440 #: documents/serialisers.py:2418 documents/serialisers.py:2428
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2435 #: documents/serialisers.py:2423
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2582 #: documents/serialisers.py:2570
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Засечена е невалидна променлива." msgstr "Засечена е невалидна променлива."
#: documents/serialisers.py:2951 #: documents/serialisers.py:2939
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2981 documents/views.py:4784 #: documents/serialisers.py:2969 documents/views.py:4763
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1911,40 +1911,40 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:335 documents/views.py:2726 #: documents/views.py:336 documents/views.py:2723
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1673 #: documents/views.py:1670
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1684 #: documents/views.py:1681
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:1696 #: documents/views.py:1693
msgid "AI backend rejected the request. Check logs for details." msgid "AI backend rejected the request. Check logs for details."
msgstr "" msgstr ""
#: documents/views.py:2551 documents/views.py:2867 #: documents/views.py:2548 documents/views.py:2864
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4797 #: documents/views.py:4776
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4843 #: documents/views.py:4822
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4907 #: documents/views.py:4886
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4921 #: documents/views.py:4900
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
@@ -2189,198 +2189,194 @@ msgid "Sets the LLM embedding model"
msgstr "" msgstr ""
#: paperless/models.py:369 #: paperless/models.py:369
msgid "Sets the LLM embedding API key"
msgstr ""
#: paperless/models.py:376
msgid "Sets the LLM embedding endpoint, optional" msgid "Sets the LLM embedding endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:383 #: paperless/models.py:376
msgid "Sets the LLM embedding chunk size" msgid "Sets the LLM embedding chunk size"
msgstr "" msgstr ""
#: paperless/models.py:389 #: paperless/models.py:382
msgid "Sets the LLM context size" msgid "Sets the LLM context size"
msgstr "" msgstr ""
#: paperless/models.py:395 #: paperless/models.py:388
msgid "Sets the LLM backend" msgid "Sets the LLM backend"
msgstr "" msgstr ""
#: paperless/models.py:403 #: paperless/models.py:396
msgid "Sets the LLM model" msgid "Sets the LLM model"
msgstr "" msgstr ""
#: paperless/models.py:410 #: paperless/models.py:403
msgid "Sets the LLM API key" msgid "Sets the LLM API key"
msgstr "" msgstr ""
#: paperless/models.py:417 #: paperless/models.py:410
msgid "Sets the LLM endpoint, optional" msgid "Sets the LLM endpoint, optional"
msgstr "" msgstr ""
#: paperless/models.py:424 #: paperless/models.py:417
msgid "Sets the LLM output language" msgid "Sets the LLM output language"
msgstr "" msgstr ""
#: paperless/models.py:431 #: paperless/models.py:424
msgid "Sets the LLM timeout in seconds" msgid "Sets the LLM timeout in seconds"
msgstr "" msgstr ""
#: paperless/models.py:437 #: paperless/models.py:430
msgid "paperless application settings" msgid "paperless application settings"
msgstr "настройки на софтуера" msgstr "настройки на софтуера"
#: paperless/settings/__init__.py:560 #: paperless/settings/__init__.py:558
msgid "English (US)" msgid "English (US)"
msgstr "Английски (САЩ)" msgstr "Английски (САЩ)"
#: paperless/settings/__init__.py:561 #: paperless/settings/__init__.py:559
msgid "Arabic" msgid "Arabic"
msgstr "Арабски" msgstr "Арабски"
#: paperless/settings/__init__.py:562 #: paperless/settings/__init__.py:560
msgid "Afrikaans" msgid "Afrikaans"
msgstr "Африканс" msgstr "Африканс"
#: paperless/settings/__init__.py:563 #: paperless/settings/__init__.py:561
msgid "Belarusian" msgid "Belarusian"
msgstr "Беларуски" msgstr "Беларуски"
#: paperless/settings/__init__.py:564 #: paperless/settings/__init__.py:562
msgid "Bulgarian" msgid "Bulgarian"
msgstr "Български" msgstr "Български"
#: paperless/settings/__init__.py:565 #: paperless/settings/__init__.py:563
msgid "Catalan" msgid "Catalan"
msgstr "Каталунски" msgstr "Каталунски"
#: paperless/settings/__init__.py:566 #: paperless/settings/__init__.py:564
msgid "Czech" msgid "Czech"
msgstr "Чешки" msgstr "Чешки"
#: paperless/settings/__init__.py:567 #: paperless/settings/__init__.py:565
msgid "Danish" msgid "Danish"
msgstr "Датски" msgstr "Датски"
#: paperless/settings/__init__.py:568 #: paperless/settings/__init__.py:566
msgid "German" msgid "German"
msgstr "Немски" msgstr "Немски"
#: paperless/settings/__init__.py:569 #: paperless/settings/__init__.py:567
msgid "Greek" msgid "Greek"
msgstr "Гръцки" msgstr "Гръцки"
#: paperless/settings/__init__.py:570 #: paperless/settings/__init__.py:568
msgid "English (GB)" msgid "English (GB)"
msgstr "Английски (Великобритания)" msgstr "Английски (Великобритания)"
#: paperless/settings/__init__.py:571 #: paperless/settings/__init__.py:569
msgid "Spanish" msgid "Spanish"
msgstr "Испански" msgstr "Испански"
#: paperless/settings/__init__.py:572 #: paperless/settings/__init__.py:570
msgid "Persian" msgid "Persian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:573 #: paperless/settings/__init__.py:571
msgid "Finnish" msgid "Finnish"
msgstr "Финландски" msgstr "Финландски"
#: paperless/settings/__init__.py:574 #: paperless/settings/__init__.py:572
msgid "French" msgid "French"
msgstr "Френски" msgstr "Френски"
#: paperless/settings/__init__.py:575 #: paperless/settings/__init__.py:573
msgid "Hungarian" msgid "Hungarian"
msgstr "Унгарски" msgstr "Унгарски"
#: paperless/settings/__init__.py:576 #: paperless/settings/__init__.py:574
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:577 #: paperless/settings/__init__.py:575
msgid "Italian" msgid "Italian"
msgstr "Италиански" msgstr "Италиански"
#: paperless/settings/__init__.py:578 #: paperless/settings/__init__.py:576
msgid "Japanese" msgid "Japanese"
msgstr "Японски" msgstr "Японски"
#: paperless/settings/__init__.py:579 #: paperless/settings/__init__.py:577
msgid "Korean" msgid "Korean"
msgstr "Корейски" msgstr "Корейски"
#: paperless/settings/__init__.py:580 #: paperless/settings/__init__.py:578
msgid "Luxembourgish" msgid "Luxembourgish"
msgstr "Люксембургски" msgstr "Люксембургски"
#: paperless/settings/__init__.py:581 #: paperless/settings/__init__.py:579
msgid "Norwegian" msgid "Norwegian"
msgstr "Норвежки" msgstr "Норвежки"
#: paperless/settings/__init__.py:582 #: paperless/settings/__init__.py:580
msgid "Dutch" msgid "Dutch"
msgstr "Холандски" msgstr "Холандски"
#: paperless/settings/__init__.py:583 #: paperless/settings/__init__.py:581
msgid "Polish" msgid "Polish"
msgstr "Полски" msgstr "Полски"
#: paperless/settings/__init__.py:584 #: paperless/settings/__init__.py:582
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "Португалски (Бразилия)" msgstr "Португалски (Бразилия)"
#: paperless/settings/__init__.py:585 #: paperless/settings/__init__.py:583
msgid "Portuguese" msgid "Portuguese"
msgstr "Португалски" msgstr "Португалски"
#: paperless/settings/__init__.py:586 #: paperless/settings/__init__.py:584
msgid "Romanian" msgid "Romanian"
msgstr "Румънски" msgstr "Румънски"
#: paperless/settings/__init__.py:587 #: paperless/settings/__init__.py:585
msgid "Russian" msgid "Russian"
msgstr "Руски" msgstr "Руски"
#: paperless/settings/__init__.py:588 #: paperless/settings/__init__.py:586
msgid "Slovak" msgid "Slovak"
msgstr "Словашки" msgstr "Словашки"
#: paperless/settings/__init__.py:589 #: paperless/settings/__init__.py:587
msgid "Slovenian" msgid "Slovenian"
msgstr "Словенски" msgstr "Словенски"
#: paperless/settings/__init__.py:590 #: paperless/settings/__init__.py:588
msgid "Serbian" msgid "Serbian"
msgstr "Сръбски" msgstr "Сръбски"
#: paperless/settings/__init__.py:591 #: paperless/settings/__init__.py:589
msgid "Swedish" msgid "Swedish"
msgstr "Шведски" msgstr "Шведски"
#: paperless/settings/__init__.py:592 #: paperless/settings/__init__.py:590
msgid "Turkish" msgid "Turkish"
msgstr "Турски" msgstr "Турски"
#: paperless/settings/__init__.py:593 #: paperless/settings/__init__.py:591
msgid "Ukrainian" msgid "Ukrainian"
msgstr "Украински" msgstr "Украински"
#: paperless/settings/__init__.py:594 #: paperless/settings/__init__.py:592
msgid "Vietnamese" msgid "Vietnamese"
msgstr "" msgstr ""
#: paperless/settings/__init__.py:595 #: paperless/settings/__init__.py:593
msgid "Chinese Simplified" msgid "Chinese Simplified"
msgstr "Китайски опростен" msgstr "Китайски опростен"
#: paperless/settings/__init__.py:596 #: paperless/settings/__init__.py:594
msgid "Chinese Traditional" msgid "Chinese Traditional"
msgstr "Китайски традиционен" msgstr "Китайски традиционен"
#: paperless/urls.py:438 #: paperless/urls.py:435
msgid "Paperless-ngx administration" msgid "Paperless-ngx administration"
msgstr "Paperless-ngx администрация" msgstr "Paperless-ngx администрация"

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