mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-07 16:47:13 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d61214bba | ||
|
|
dcfe389909 | ||
|
|
0beb0a1d0b | ||
|
|
bfaea71a83 | ||
|
|
0b7cecb6bb |
No files matched your search
@@ -80,7 +80,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.backend_changed == 'true'
|
if: needs.changes.outputs.backend_changed == 'true'
|
||||||
name: "Python ${{ matrix.python-version }}"
|
name: "Python ${{ matrix.python-version }}"
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
strategy:
|
strategy:
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
packages: unpaper tesseract-ocr imagemagick ghostscript poppler-utils
|
packages: 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-7/policy.xml
|
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
|
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.backend_changed == 'true'
|
if: needs.changes.outputs.backend_changed == 'true'
|
||||||
name: Check project typing
|
name: Check project typing
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- runner: ubuntu-26.04
|
- runner: ubuntu-24.04
|
||||||
arch: amd64
|
arch: amd64
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
- runner: ubuntu-26.04-arm
|
- runner: ubuntu-24.04-arm
|
||||||
arch: arm64
|
arch: arm64
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
@@ -163,7 +163,7 @@ jobs:
|
|||||||
archive: false
|
archive: false
|
||||||
merge-and-push:
|
merge-and-push:
|
||||||
name: Merge and Push Manifest
|
name: Merge and Push Manifest
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build-arch
|
needs: build-arch
|
||||||
if: needs.build-arch.outputs.should-push == 'true'
|
if: needs.build-arch.outputs.should-push == 'true'
|
||||||
environment: image-publishing
|
environment: image-publishing
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.docs_changed == 'true'
|
if: needs.changes.outputs.docs_changed == 'true'
|
||||||
name: Build Documentation
|
name: Build Documentation
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
name: Deploy Documentation
|
name: Deploy Documentation
|
||||||
needs: [changes, build]
|
needs: [changes, build]
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.docs_changed == 'true'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.docs_changed == 'true'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
pages: write
|
pages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
name: Lint
|
name: Lint
|
||||||
needs: [changes, install-dependencies]
|
needs: [changes, install-dependencies]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||||
needs: [changes, install-dependencies]
|
needs: [changes, install-dependencies]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
strategy:
|
strategy:
|
||||||
@@ -188,10 +188,10 @@ jobs:
|
|||||||
name: E2E Tests
|
name: E2E Tests
|
||||||
needs: [changes, install-dependencies]
|
needs: [changes, install-dependencies]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
container: mcr.microsoft.com/playwright:v1.62.1-resolute
|
container: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
@@ -246,7 +246,7 @@ jobs:
|
|||||||
name: Frontend Build
|
name: Frontend Build
|
||||||
needs: [changes, unit-tests, e2e-tests]
|
needs: [changes, unit-tests, e2e-tests]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
wait-for-docker:
|
wait-for-docker:
|
||||||
name: Wait for Docker Build
|
name: Wait for Docker Build
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
checks: read
|
checks: read
|
||||||
statuses: read
|
statuses: read
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
build-release:
|
build-release:
|
||||||
name: Build Release
|
name: Build Release
|
||||||
needs: wait-for-docker
|
needs: wait-for-docker
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
timeout-minutes: 12
|
timeout-minutes: 12
|
||||||
uses: $/.github/actions/apt-install
|
uses: $/.github/actions/apt-install
|
||||||
with:
|
with:
|
||||||
packages: gettext libleptonica6
|
packages: gettext liblept5
|
||||||
# ---- Build Documentation ----
|
# ---- Build Documentation ----
|
||||||
- name: Build documentation
|
- name: Build documentation
|
||||||
env:
|
env:
|
||||||
@@ -145,7 +145,7 @@ jobs:
|
|||||||
publish-release:
|
publish-release:
|
||||||
name: Publish Release
|
name: Publish Release
|
||||||
needs: build-release
|
needs: build-release
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -197,7 +197,7 @@ jobs:
|
|||||||
name: Append Changelog
|
name: Append Changelog
|
||||||
needs: publish-release
|
needs: publish-release
|
||||||
if: needs.publish-release.outputs.prerelease == 'false'
|
if: needs.publish-release.outputs.prerelease == 'false'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
zizmor:
|
zizmor:
|
||||||
name: Run zizmor
|
name: Run zizmor
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||||
semgrep:
|
semgrep:
|
||||||
name: Semgrep CE
|
name: Semgrep CE
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
container:
|
container:
|
||||||
image: semgrep/semgrep:1.155.0@sha256:cc869c685dcc0fe497c86258da9f205397d8108e56d21a86082ea4886e52784d
|
image: semgrep/semgrep:1.155.0@sha256:cc869c685dcc0fe497c86258da9f205397d8108e56d21a86082ea4886e52784d
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
cleanup-images:
|
cleanup-images:
|
||||||
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
environment: registry-maintenance
|
environment: registry-maintenance
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
cleanup-untagged-images:
|
cleanup-untagged-images:
|
||||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- cleanup-images
|
- cleanup-images
|
||||||
environment: registry-maintenance
|
environment: registry-maintenance
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
name: Crowdin Sync
|
name: Crowdin Sync
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
environment: translation-sync
|
environment: translation-sync
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
# Note: peakoss/anti-slop does not support the `issues` event yet (all of its
|
# Note: peakoss/anti-slop does not support the `issues` event yet (all of its
|
||||||
# issue inputs are still commented out upstream), so the checks that the PR Bot
|
# issue inputs are still commented out upstream), so the checks that the PR Bot
|
||||||
# workflow gets from the action are implemented manually here.
|
# workflow gets from the action are implemented manually here.
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
types: [opened]
|
types: [opened]
|
||||||
jobs:
|
jobs:
|
||||||
Anti-slop:
|
Anti-slop:
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: read
|
issues: read
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
ASLOP-PR-VERIFY
|
ASLOP-PR-VERIFY
|
||||||
pr-bot:
|
pr-bot:
|
||||||
name: Automated PR Bot
|
name: Automated PR Bot
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-latest
|
||||||
# Runs after Anti-slop so the welcome comment can see whether the PR was closed
|
# Runs after Anti-slop so the welcome comment can see whether the PR was closed
|
||||||
# instead of racing it. Still runs if that job fails, so labeling is not lost.
|
# instead of racing it. Still runs if that job fails, so labeling is not lost.
|
||||||
needs: Anti-slop
|
needs: Anti-slop
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
pr_opened_or_reopened:
|
pr_opened_or_reopened:
|
||||||
name: pr_opened_or_reopened
|
name: pr_opened_or_reopened
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
# write permission is required for autolabeler
|
# write permission is required for autolabeler
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
name: 'Stale'
|
name: 'Stale'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
lock-threads:
|
lock-threads:
|
||||||
name: 'Lock Old Threads'
|
name: 'Lock Old Threads'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
close-answered-discussions:
|
close-answered-discussions:
|
||||||
name: 'Close Answered Discussions'
|
name: 'Close Answered Discussions'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
discussions: write
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
close-outdated-discussions:
|
close-outdated-discussions:
|
||||||
name: 'Close Outdated Discussions'
|
name: 'Close Outdated Discussions'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
discussions: write
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
close-unsupported-feature-requests:
|
close-unsupported-feature-requests:
|
||||||
name: 'Close Unsupported Feature Requests'
|
name: 'Close Unsupported Feature Requests'
|
||||||
if: github.repository_owner == 'paperless-ngx'
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
discussions: write
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
generate-translate-strings:
|
generate-translate-strings:
|
||||||
name: Generate Translation Strings
|
name: Generate Translation Strings
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-latest
|
||||||
environment: translation-sync
|
environment: translation-sync
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ src/documents/bulk_edit.py:0: error: Incompatible types in assignment (expressio
|
|||||||
src/documents/bulk_edit.py:0: error: Invalid index type "str" for "dict[FieldDataType, str]"; expected type "FieldDataType" [index]
|
src/documents/bulk_edit.py:0: error: Invalid index type "str" for "dict[FieldDataType, str]"; expected type "FieldDataType" [index]
|
||||||
src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, Any]]; expected List[int] [misc]
|
src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, Any]]; expected List[int] [misc]
|
||||||
src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, None]]; expected List[int] [misc]
|
src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, None]]; expected List[int] [misc]
|
||||||
src/documents/bulk_edit.py:0: error: Missing named argument "p" for "remove" of "PageList" [call-arg]
|
|
||||||
src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg]
|
src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg]
|
||||||
src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg]
|
src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg]
|
||||||
src/documents/bulk_edit.py:0: error: Need type annotation for "to_create" (hint: "to_create: list[<type>] = ...") [var-annotated]
|
src/documents/bulk_edit.py:0: error: Need type annotation for "to_create" (hint: "to_create: list[<type>] = ...") [var-annotated]
|
||||||
|
|||||||
@@ -91,13 +91,6 @@
|
|||||||
"concise_description": "Argument `list[int]` is not assignable to parameter `args` with type `tuple[Any, ...] | None` in function `celery.app.task.Task.apply_async`",
|
"concise_description": "Argument `list[int]` is not assignable to parameter `args` with type `tuple[Any, ...] | None` in function `celery.app.task.Task.apply_async`",
|
||||||
"severity": "error"
|
"severity": "error"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"column": 33,
|
|
||||||
"path": "src/documents/bulk_edit.py",
|
|
||||||
"name": "missing-argument",
|
|
||||||
"concise_description": "Missing argument `p` in function `pikepdf._core.PageList.remove`",
|
|
||||||
"severity": "error"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"column": 25,
|
"column": 25,
|
||||||
"path": "src/documents/caching.py",
|
"path": "src/documents/caching.py",
|
||||||
|
|||||||
@@ -1010,19 +1010,6 @@ documents to both separate and categorize them in a single operation.
|
|||||||
**Example:** A 6-page scan with TAG:invoice on page 3 and TAG:receipt on page 5 will create
|
**Example:** A 6-page scan with TAG:invoice on page 3 and TAG:receipt on page 5 will create
|
||||||
three documents: pages 1-2 (no tags), pages 3-4 (tagged "invoice"), and pages 5-6 (tagged "receipt").
|
three documents: pages 1-2 (no tags), pages 3-4 (tagged "invoice"), and pages 5-6 (tagged "receipt").
|
||||||
|
|
||||||
### Barcode Contents {#barcode-contents}
|
|
||||||
|
|
||||||
By default, Paperless only uses barcodes for splitting, ASNs and tags. With
|
|
||||||
[`PAPERLESS_CONSUMER_STORE_BARCODE_VALUES`](configuration.md#PAPERLESS_CONSUMER_STORE_BARCODE_VALUES)
|
|
||||||
enabled, it stores the content of every barcode with the document, e.g. payment codes or QR codes.
|
|
||||||
|
|
||||||
- Barcodes are listed on the **Metadata** tab with page, type and content, and can be copied.
|
|
||||||
- The API returns them in the `barcodes` field of `/api/documents/{id}/metadata/`.
|
|
||||||
- They can be [searched](usage.md#searching-barcodes), e.g. `barcodes:DE89370400440532013000`.
|
|
||||||
- Only the first [`PAPERLESS_CONSUMER_BARCODE_MAX_PAGES`](configuration.md#PAPERLESS_CONSUMER_BARCODE_MAX_PAGES)
|
|
||||||
pages are scanned. Reprocessing reads the barcodes of existing documents.
|
|
||||||
- Each version keeps its own barcodes, and the newest version's are shown and searched.
|
|
||||||
|
|
||||||
## Automatic collation of double-sided documents {#collate}
|
## Automatic collation of double-sided documents {#collate}
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|||||||
@@ -1796,13 +1796,6 @@ assigns or creates tags if a properly formatted barcode is detected.
|
|||||||
|
|
||||||
Defaults to false.
|
Defaults to false.
|
||||||
|
|
||||||
#### [`PAPERLESS_CONSUMER_STORE_BARCODE_VALUES=<bool>`](#PAPERLESS_CONSUMER_STORE_BARCODE_VALUES) {#PAPERLESS_CONSUMER_STORE_BARCODE_VALUES}
|
|
||||||
|
|
||||||
: Stores the content of every barcode found during consumption, see
|
|
||||||
[Barcode Contents](advanced_usage.md#barcode-contents).
|
|
||||||
|
|
||||||
Defaults to false.
|
|
||||||
|
|
||||||
## Audit Trail
|
## Audit Trail
|
||||||
|
|
||||||
#### [`PAPERLESS_AUDIT_LOG_ENABLED=<bool>`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED}
|
#### [`PAPERLESS_AUDIT_LOG_ENABLED=<bool>`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED}
|
||||||
|
|||||||
@@ -1061,20 +1061,6 @@ notes.user:alice notes.note:insurance
|
|||||||
|
|
||||||
The bare `notes:` prefix is shorthand for `notes.note:`.
|
The bare `notes:` prefix is shorthand for `notes.note:`.
|
||||||
|
|
||||||
#### Searching barcodes
|
|
||||||
|
|
||||||
If [barcode contents are stored](advanced_usage.md#barcode-contents), they can be searched by
|
|
||||||
content or type, but only with a field name:
|
|
||||||
|
|
||||||
```
|
|
||||||
barcodes.value:DE89370400440532013000
|
|
||||||
barcodes.format:qrcode
|
|
||||||
barcodes:wifi barcodes:guest
|
|
||||||
```
|
|
||||||
|
|
||||||
`barcodes:` is shorthand for `barcodes.value:`. Separators are stripped, so each part of e.g.
|
|
||||||
`WIFI:S:Guest;P:secret;;` can be searched on its own.
|
|
||||||
|
|
||||||
All of these can be combined. Syntax not described here may not work as expected, and an unknown field name is searched as ordinary text.
|
All of these can be combined. Syntax not described here may not work as expected, and an unknown field name is searched as ordinary text.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|||||||
+2
-2
@@ -33,7 +33,7 @@ dependencies = [
|
|||||||
"django-compression-middleware~=0.5.0",
|
"django-compression-middleware~=0.5.0",
|
||||||
"django-cors-headers~=4.9.0",
|
"django-cors-headers~=4.9.0",
|
||||||
"django-extensions~=4.1",
|
"django-extensions~=4.1",
|
||||||
"django-filter>=25.1,<27",
|
"django-filter~=25.1",
|
||||||
"django-guardian>=3.3.3,<3.6",
|
"django-guardian>=3.3.3,<3.6",
|
||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=1.0.1",
|
||||||
"django-rich~=2.2.0",
|
"django-rich~=2.2.0",
|
||||||
@@ -90,7 +90,7 @@ 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.3",
|
"psycopg-pool==3.3.2",
|
||||||
]
|
]
|
||||||
webserver = [
|
webserver = [
|
||||||
"granian[uvloop]>=2.7,<2.9",
|
"granian[uvloop]>=2.7,<2.9",
|
||||||
|
|||||||
+113
-150
@@ -698,7 +698,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">463,464</context>
|
<context context-type="linenumber">457,458</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
|
||||||
@@ -870,7 +870,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">482</context>
|
<context context-type="linenumber">476</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
@@ -1373,7 +1373,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1907</context>
|
<context context-type="linenumber">1901</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1577733187050997705" datatype="html">
|
<trans-unit id="1577733187050997705" datatype="html">
|
||||||
@@ -1451,7 +1451,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">408,409</context>
|
<context context-type="linenumber">402,403</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -1700,7 +1700,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">179</context>
|
<context context-type="linenumber">177</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2691296884221415710" datatype="html">
|
<trans-unit id="2691296884221415710" datatype="html">
|
||||||
@@ -1715,7 +1715,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">184</context>
|
<context context-type="linenumber">182</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -1750,7 +1750,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">188</context>
|
<context context-type="linenumber">186</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -1785,7 +1785,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">192</context>
|
<context context-type="linenumber">190</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -2478,7 +2478,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">713</context>
|
<context context-type="linenumber">711</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
|
||||||
@@ -3392,11 +3392,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1521</context>
|
<context context-type="linenumber">1515</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1908</context>
|
<context context-type="linenumber">1902</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -4012,7 +4012,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1474</context>
|
<context context-type="linenumber">1468</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -4156,7 +4156,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1961</context>
|
<context context-type="linenumber">1955</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6661109599266152398" datatype="html">
|
<trans-unit id="6661109599266152398" datatype="html">
|
||||||
@@ -4167,7 +4167,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1962</context>
|
<context context-type="linenumber">1956</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5162686434580248853" datatype="html">
|
<trans-unit id="5162686434580248853" datatype="html">
|
||||||
@@ -4178,7 +4178,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1963</context>
|
<context context-type="linenumber">1957</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6665634854532231106" datatype="html">
|
<trans-unit id="6665634854532231106" datatype="html">
|
||||||
@@ -5430,7 +5430,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">374,375</context>
|
<context context-type="linenumber">368,369</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8057014866157903311" datatype="html">
|
<trans-unit id="8057014866157903311" datatype="html">
|
||||||
@@ -6311,7 +6311,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1478</context>
|
<context context-type="linenumber">1472</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -6344,7 +6344,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">177</context>
|
<context context-type="linenumber">175</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
@@ -7176,10 +7176,6 @@
|
|||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">56,57</context>
|
<context context-type="linenumber">56,57</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-barcodes/document-barcodes.component.html</context>
|
|
||||||
<context context-type="linenumber">5,6</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1230154438678955604" datatype="html">
|
<trans-unit id="1230154438678955604" datatype="html">
|
||||||
<source>Change</source>
|
<source>Change</source>
|
||||||
@@ -7752,8 +7748,8 @@
|
|||||||
<context context-type="linenumber">120</context>
|
<context context-type="linenumber">120</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5377184518735933155" datatype="html">
|
<trans-unit id="5700628356844396417" datatype="html">
|
||||||
<source>{VAR_PLURAL, plural, =1 {1 suggestion available below} other {<x id="INTERPOLATION"/> suggestions available below}}</source>
|
<source>{VAR_PLURAL, plural, =1 {1 existing value suggested below} other {<x id="INTERPOLATION"/> existing values suggested below}}</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">54</context>
|
<context context-type="linenumber">54</context>
|
||||||
@@ -8337,10 +8333,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1407560924967345762" datatype="html">
|
<trans-unit id="1407560924967345762" datatype="html">
|
||||||
<source>Page</source>
|
<source>Page</source>
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-barcodes/document-barcodes.component.html</context>
|
|
||||||
<context context-type="linenumber">4,5</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">5,6</context>
|
<context context-type="linenumber">5,6</context>
|
||||||
@@ -8354,28 +8346,6 @@
|
|||||||
<context context-type="linenumber">34</context>
|
<context context-type="linenumber">34</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6205355627445317276" datatype="html">
|
|
||||||
<source>Content</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-barcodes/document-barcodes.component.html</context>
|
|
||||||
<context context-type="linenumber">6,7</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">300,301</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3846359579066496296" datatype="html">
|
|
||||||
<source>Copy content</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-barcodes/document-barcodes.component.html</context>
|
|
||||||
<context context-type="linenumber">32,33</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-barcodes/document-barcodes.component.html</context>
|
|
||||||
<context context-type="linenumber">40,41</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2266163016683537825" datatype="html">
|
<trans-unit id="2266163016683537825" datatype="html">
|
||||||
<source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages()}}"/></source>
|
<source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages()}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -8507,32 +8477,39 @@
|
|||||||
<source>Details</source>
|
<source>Details</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">174,175</context>
|
<context context-type="linenumber">172,173</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5114742157723900905" datatype="html">
|
<trans-unit id="5114742157723900905" datatype="html">
|
||||||
<source>Date created</source>
|
<source>Date created</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">181</context>
|
<context context-type="linenumber">179</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5607669932062416162" datatype="html">
|
<trans-unit id="5607669932062416162" datatype="html">
|
||||||
<source>Default</source>
|
<source>Default</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">193</context>
|
<context context-type="linenumber">191</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">71</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6205355627445317276" datatype="html">
|
||||||
|
<source>Content</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
|
<context context-type="linenumber">298,299</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="218403386307979629" datatype="html">
|
<trans-unit id="218403386307979629" datatype="html">
|
||||||
<source>Metadata</source>
|
<source>Metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">309,310</context>
|
<context context-type="linenumber">307,308</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
|
||||||
@@ -8543,235 +8520,228 @@
|
|||||||
<source>Date modified</source>
|
<source>Date modified</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">316,317</context>
|
<context context-type="linenumber">314,315</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6392918669949841614" datatype="html">
|
<trans-unit id="6392918669949841614" datatype="html">
|
||||||
<source>Date added</source>
|
<source>Date added</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">320,321</context>
|
<context context-type="linenumber">318,319</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="146828917013192897" datatype="html">
|
<trans-unit id="146828917013192897" datatype="html">
|
||||||
<source>Media filename</source>
|
<source>Media filename</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">324,325</context>
|
<context context-type="linenumber">322,323</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4500855521601039868" datatype="html">
|
<trans-unit id="4500855521601039868" datatype="html">
|
||||||
<source>Original filename</source>
|
<source>Original filename</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">328,329</context>
|
<context context-type="linenumber">326,327</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2659735245739197634" datatype="html">
|
<trans-unit id="2659735245739197634" datatype="html">
|
||||||
<source>Original SHA256 checksum</source>
|
<source>Original SHA256 checksum</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">332,333</context>
|
<context context-type="linenumber">330,331</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5888243105821763422" datatype="html">
|
<trans-unit id="5888243105821763422" datatype="html">
|
||||||
<source>Original file size</source>
|
<source>Original file size</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">336,337</context>
|
<context context-type="linenumber">334,335</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2696647325713149563" datatype="html">
|
<trans-unit id="2696647325713149563" datatype="html">
|
||||||
<source>Original mime type</source>
|
<source>Original mime type</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">340,341</context>
|
<context context-type="linenumber">338,339</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6714358112223607756" datatype="html">
|
<trans-unit id="6714358112223607756" datatype="html">
|
||||||
<source>Archive SHA256 checksum</source>
|
<source>Archive SHA256 checksum</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">345,346</context>
|
<context context-type="linenumber">343,344</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6033581412811562084" datatype="html">
|
<trans-unit id="6033581412811562084" datatype="html">
|
||||||
<source>Archive file size</source>
|
<source>Archive file size</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">351,352</context>
|
<context context-type="linenumber">349,350</context>
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8459338343197260257" datatype="html">
|
|
||||||
<source>Barcodes</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">360,361</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6992781481378431874" datatype="html">
|
<trans-unit id="6992781481378431874" datatype="html">
|
||||||
<source>Original document metadata</source>
|
<source>Original document metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">364</context>
|
<context context-type="linenumber">358</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2846565152091361585" datatype="html">
|
<trans-unit id="2846565152091361585" datatype="html">
|
||||||
<source>Archived document metadata</source>
|
<source>Archived document metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">367</context>
|
<context context-type="linenumber">361</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7206723502037428235" datatype="html">
|
<trans-unit id="7206723502037428235" datatype="html">
|
||||||
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document()?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="{{document().notes.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document()?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="{{document().notes.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">386,389</context>
|
<context context-type="linenumber">380,383</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="186236568870281953" datatype="html">
|
<trans-unit id="186236568870281953" datatype="html">
|
||||||
<source>History</source>
|
<source>History</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">397,398</context>
|
<context context-type="linenumber">391,392</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8236092845697214347" datatype="html">
|
<trans-unit id="8236092845697214347" datatype="html">
|
||||||
<source> Duplicates <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="{{ document().duplicate_documents.length }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source>
|
<source> Duplicates <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="{{ document().duplicate_documents.length }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">420,423</context>
|
<context context-type="linenumber">414,417</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6449374629822973702" datatype="html">
|
<trans-unit id="6449374629822973702" datatype="html">
|
||||||
<source>Duplicate documents detected:</source>
|
<source>Duplicate documents detected:</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">425,426</context>
|
<context context-type="linenumber">419,420</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="14058600336670816" datatype="html">
|
<trans-unit id="14058600336670816" datatype="html">
|
||||||
<source>In trash</source>
|
<source>In trash</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">436,437</context>
|
<context context-type="linenumber">430,431</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5129524307369213584" datatype="html">
|
<trans-unit id="5129524307369213584" datatype="html">
|
||||||
<source>Save & next</source>
|
<source>Save & next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">465</context>
|
<context context-type="linenumber">459</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4910102545766233758" datatype="html">
|
<trans-unit id="4910102545766233758" datatype="html">
|
||||||
<source>Save & close</source>
|
<source>Save & close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">467</context>
|
<context context-type="linenumber">461</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3823219296477075982" datatype="html">
|
<trans-unit id="3823219296477075982" datatype="html">
|
||||||
<source>Discard</source>
|
<source>Discard</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">469,470</context>
|
<context context-type="linenumber">463,464</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1309556917227148591" datatype="html">
|
<trans-unit id="1309556917227148591" datatype="html">
|
||||||
<source>Document loading...</source>
|
<source>Document loading...</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">477</context>
|
<context context-type="linenumber">471</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8191371354890763172" datatype="html">
|
<trans-unit id="8191371354890763172" datatype="html">
|
||||||
<source>Enter Password</source>
|
<source>Enter Password</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">531</context>
|
<context context-type="linenumber">525</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5758784066858623886" datatype="html">
|
<trans-unit id="5758784066858623886" datatype="html">
|
||||||
<source>Error retrieving metadata</source>
|
<source>Error retrieving metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">463</context>
|
<context context-type="linenumber">461</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2218903673684131427" datatype="html">
|
<trans-unit id="2218903673684131427" datatype="html">
|
||||||
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">566,568</context>
|
<context context-type="linenumber">564,566</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1047,1049</context>
|
<context context-type="linenumber">1045,1047</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6357361810318120957" datatype="html">
|
<trans-unit id="6357361810318120957" datatype="html">
|
||||||
<source>Document was updated</source>
|
<source>Document was updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">708</context>
|
<context context-type="linenumber">706</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5154064822428631306" datatype="html">
|
<trans-unit id="5154064822428631306" datatype="html">
|
||||||
<source>Document was updated at <x id="PH" equiv-text="formattedModified"/>.</source>
|
<source>Document was updated at <x id="PH" equiv-text="formattedModified"/>.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">709</context>
|
<context context-type="linenumber">707</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8462497568316256794" datatype="html">
|
<trans-unit id="8462497568316256794" datatype="html">
|
||||||
<source>Reload to discard your local unsaved edits and load the latest remote version.</source>
|
<source>Reload to discard your local unsaved edits and load the latest remote version.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">710</context>
|
<context context-type="linenumber">708</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7967484035994732534" datatype="html">
|
<trans-unit id="7967484035994732534" datatype="html">
|
||||||
<source>Reload</source>
|
<source>Reload</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">712</context>
|
<context context-type="linenumber">710</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2907037627372942104" datatype="html">
|
<trans-unit id="2907037627372942104" datatype="html">
|
||||||
<source>Document reloaded with latest changes.</source>
|
<source>Document reloaded with latest changes.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">768</context>
|
<context context-type="linenumber">766</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6435639868943916539" datatype="html">
|
<trans-unit id="6435639868943916539" datatype="html">
|
||||||
<source>Document reloaded.</source>
|
<source>Document reloaded.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">779</context>
|
<context context-type="linenumber">777</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6142395741265832184" datatype="html">
|
<trans-unit id="6142395741265832184" datatype="html">
|
||||||
<source>Next document</source>
|
<source>Next document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">881</context>
|
<context context-type="linenumber">879</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="651985345816518480" datatype="html">
|
<trans-unit id="651985345816518480" datatype="html">
|
||||||
<source>Previous document</source>
|
<source>Previous document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">891</context>
|
<context context-type="linenumber">889</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2885986061416655600" datatype="html">
|
<trans-unit id="2885986061416655600" datatype="html">
|
||||||
<source>Close document</source>
|
<source>Close document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">899</context>
|
<context context-type="linenumber">897</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
||||||
@@ -8782,28 +8752,28 @@
|
|||||||
<source>Save document</source>
|
<source>Save document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">906</context>
|
<context context-type="linenumber">904</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1784543155727940353" datatype="html">
|
<trans-unit id="1784543155727940353" datatype="html">
|
||||||
<source>Save and close / next</source>
|
<source>Save and close / next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">915</context>
|
<context context-type="linenumber">913</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7427704425579737895" datatype="html">
|
<trans-unit id="7427704425579737895" datatype="html">
|
||||||
<source>Error retrieving version content</source>
|
<source>Error retrieving version content</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1029</context>
|
<context context-type="linenumber">1027</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="159901853873315050" datatype="html">
|
<trans-unit id="159901853873315050" datatype="html">
|
||||||
<source>Unsaved Changes</source>
|
<source>Unsaved Changes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1073</context>
|
<context context-type="linenumber">1071</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/guards/dirty-form.guard.ts</context>
|
<context context-type="sourcefile">src/app/guards/dirty-form.guard.ts</context>
|
||||||
@@ -8826,74 +8796,74 @@
|
|||||||
<source>You have unsaved changes to the content of this version.</source>
|
<source>You have unsaved changes to the content of this version.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1074</context>
|
<context context-type="linenumber">1072</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="85184271222513014" datatype="html">
|
<trans-unit id="85184271222513014" datatype="html">
|
||||||
<source>Switching versions will discard them.</source>
|
<source>Switching versions will discard them.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1075</context>
|
<context context-type="linenumber">1073</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2565707334844767610" datatype="html">
|
<trans-unit id="2565707334844767610" datatype="html">
|
||||||
<source>Discard and switch</source>
|
<source>Discard and switch</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1077</context>
|
<context context-type="linenumber">1075</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2109314380040637387" datatype="html">
|
<trans-unit id="2109314380040637387" datatype="html">
|
||||||
<source>Save and switch</source>
|
<source>Save and switch</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1079</context>
|
<context context-type="linenumber">1077</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3456881259945295697" datatype="html">
|
<trans-unit id="3456881259945295697" datatype="html">
|
||||||
<source>Error retrieving suggestions.</source>
|
<source>Error retrieving suggestions.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1122</context>
|
<context context-type="linenumber">1120</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2194092841814123758" datatype="html">
|
<trans-unit id="2194092841814123758" datatype="html">
|
||||||
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1353</context>
|
<context context-type="linenumber">1347</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1381</context>
|
<context context-type="linenumber">1375</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6626387786259219838" datatype="html">
|
<trans-unit id="6626387786259219838" datatype="html">
|
||||||
<source>Error saving document "<x id="PH" equiv-text="this.document().title"/>"</source>
|
<source>Error saving document "<x id="PH" equiv-text="this.document().title"/>"</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1387</context>
|
<context context-type="linenumber">1381</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="448882439049417053" datatype="html">
|
<trans-unit id="448882439049417053" datatype="html">
|
||||||
<source>Error saving document</source>
|
<source>Error saving document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1442</context>
|
<context context-type="linenumber">1436</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8410796510716511826" datatype="html">
|
<trans-unit id="8410796510716511826" datatype="html">
|
||||||
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document().title"/>" to the trash?</source>
|
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document().title"/>" to the trash?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1475</context>
|
<context context-type="linenumber">1469</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="282586936710748252" datatype="html">
|
<trans-unit id="282586936710748252" datatype="html">
|
||||||
<source>Documents can be restored prior to permanent deletion.</source>
|
<source>Documents can be restored prior to permanent deletion.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1476</context>
|
<context context-type="linenumber">1470</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -8904,14 +8874,14 @@
|
|||||||
<source>Error deleting document</source>
|
<source>Error deleting document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1497</context>
|
<context context-type="linenumber">1491</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="619486176823357521" datatype="html">
|
<trans-unit id="619486176823357521" datatype="html">
|
||||||
<source>Reprocess confirm</source>
|
<source>Reprocess confirm</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1517</context>
|
<context context-type="linenumber">1511</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -8922,102 +8892,102 @@
|
|||||||
<source>This operation will permanently recreate the archive file for this document.</source>
|
<source>This operation will permanently recreate the archive file for this document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1518</context>
|
<context context-type="linenumber">1512</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="302054111564709516" datatype="html">
|
<trans-unit id="302054111564709516" datatype="html">
|
||||||
<source>The archive file will be re-generated with the current settings.</source>
|
<source>The archive file will be re-generated with the current settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1519</context>
|
<context context-type="linenumber">1513</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4700389117298802932" datatype="html">
|
<trans-unit id="4700389117298802932" datatype="html">
|
||||||
<source>Reprocess operation for "<x id="PH" equiv-text="this.document().title"/>" will begin in the background.</source>
|
<source>Reprocess operation for "<x id="PH" equiv-text="this.document().title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1532</context>
|
<context context-type="linenumber">1526</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4409560272830824468" datatype="html">
|
<trans-unit id="4409560272830824468" datatype="html">
|
||||||
<source>Error executing operation</source>
|
<source>Error executing operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1543</context>
|
<context context-type="linenumber">1537</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6030453331794586802" datatype="html">
|
<trans-unit id="6030453331794586802" datatype="html">
|
||||||
<source>Error downloading document</source>
|
<source>Error downloading document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1607</context>
|
<context context-type="linenumber">1601</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4458954481601077369" datatype="html">
|
<trans-unit id="4458954481601077369" datatype="html">
|
||||||
<source>Page Fit</source>
|
<source>Page Fit</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1685</context>
|
<context context-type="linenumber">1679</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4663705961777238777" datatype="html">
|
<trans-unit id="4663705961777238777" datatype="html">
|
||||||
<source>PDF edit operation for "<x id="PH" equiv-text="this.document().title"/>" will begin in the background.</source>
|
<source>PDF edit operation for "<x id="PH" equiv-text="this.document().title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1928</context>
|
<context context-type="linenumber">1922</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9043972994040261999" datatype="html">
|
<trans-unit id="9043972994040261999" datatype="html">
|
||||||
<source>Error executing PDF edit operation</source>
|
<source>Error executing PDF edit operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1940</context>
|
<context context-type="linenumber">1934</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6172690334763056188" datatype="html">
|
<trans-unit id="6172690334763056188" datatype="html">
|
||||||
<source>Please enter the current password before attempting to remove it.</source>
|
<source>Please enter the current password before attempting to remove it.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1951</context>
|
<context context-type="linenumber">1945</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="968660764814228922" datatype="html">
|
<trans-unit id="968660764814228922" datatype="html">
|
||||||
<source>Password removal operation for "<x id="PH" equiv-text="this.document().title"/>" will begin in the background.</source>
|
<source>Password removal operation for "<x id="PH" equiv-text="this.document().title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1985</context>
|
<context context-type="linenumber">1979</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2282118435712883014" datatype="html">
|
<trans-unit id="2282118435712883014" datatype="html">
|
||||||
<source>Error executing password removal operation</source>
|
<source>Error executing password removal operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1999</context>
|
<context context-type="linenumber">1993</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740891324955700797" datatype="html">
|
<trans-unit id="3740891324955700797" datatype="html">
|
||||||
<source>Print failed.</source>
|
<source>Print failed.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">2049</context>
|
<context context-type="linenumber">2043</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6457245677384603573" datatype="html">
|
<trans-unit id="6457245677384603573" datatype="html">
|
||||||
<source>Error loading document for printing.</source>
|
<source>Error loading document for printing.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">2058</context>
|
<context context-type="linenumber">2052</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6085793215710522488" datatype="html">
|
<trans-unit id="6085793215710522488" datatype="html">
|
||||||
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">2141</context>
|
<context context-type="linenumber">2135</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">2147</context>
|
<context context-type="linenumber">2141</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4958946940233632319" datatype="html">
|
<trans-unit id="4958946940233632319" datatype="html">
|
||||||
@@ -12083,130 +12053,123 @@
|
|||||||
<context context-type="linenumber">328</context>
|
<context context-type="linenumber">328</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="906679600349814230" datatype="html">
|
|
||||||
<source>Store Barcode Contents</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
|
||||||
<context context-type="linenumber">335</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7011909364081812031" datatype="html">
|
<trans-unit id="7011909364081812031" datatype="html">
|
||||||
<source>AI Enabled</source>
|
<source>AI Enabled</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">342</context>
|
<context context-type="linenumber">335</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8028880048909383956" datatype="html">
|
<trans-unit id="8028880048909383956" datatype="html">
|
||||||
<source>Consider privacy implications when enabling AI features, especially if using a remote model.</source>
|
<source>Consider privacy implications when enabling AI features, especially if using a remote model.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">346</context>
|
<context context-type="linenumber">339</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8131374115579345652" datatype="html">
|
<trans-unit id="8131374115579345652" datatype="html">
|
||||||
<source>LLM Embedding Backend</source>
|
<source>LLM Embedding Backend</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">350</context>
|
<context context-type="linenumber">343</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6647708571891295756" datatype="html">
|
<trans-unit id="6647708571891295756" datatype="html">
|
||||||
<source>LLM Embedding Model</source>
|
<source>LLM Embedding Model</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">358</context>
|
<context context-type="linenumber">351</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="861068592166833023" datatype="html">
|
<trans-unit id="861068592166833023" datatype="html">
|
||||||
<source>LLM Embedding API Key</source>
|
<source>LLM Embedding API Key</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">365</context>
|
<context context-type="linenumber">358</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2929108042259892948" datatype="html">
|
<trans-unit id="2929108042259892948" datatype="html">
|
||||||
<source>Used for embeddings when set, otherwise LLM API key is used.</source>
|
<source>Used for embeddings when set, otherwise LLM API key is used.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">367</context>
|
<context context-type="linenumber">360</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3554114880473286122" datatype="html">
|
<trans-unit id="3554114880473286122" datatype="html">
|
||||||
<source>LLM Embedding Endpoint</source>
|
<source>LLM Embedding Endpoint</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">373</context>
|
<context context-type="linenumber">366</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1044242175651289991" datatype="html">
|
<trans-unit id="1044242175651289991" datatype="html">
|
||||||
<source>LLM Embedding Chunk Size</source>
|
<source>LLM Embedding Chunk Size</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">380</context>
|
<context context-type="linenumber">373</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7218245223139363113" datatype="html">
|
<trans-unit id="7218245223139363113" datatype="html">
|
||||||
<source>LLM Context Size</source>
|
<source>LLM Context Size</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">387</context>
|
<context context-type="linenumber">380</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4234495692726214397" datatype="html">
|
<trans-unit id="4234495692726214397" datatype="html">
|
||||||
<source>LLM Backend</source>
|
<source>LLM Backend</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">394</context>
|
<context context-type="linenumber">387</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7935234833834000002" datatype="html">
|
<trans-unit id="7935234833834000002" datatype="html">
|
||||||
<source>LLM Model</source>
|
<source>LLM Model</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">402</context>
|
<context context-type="linenumber">395</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1980550530387803165" datatype="html">
|
<trans-unit id="1980550530387803165" datatype="html">
|
||||||
<source>LLM API Key</source>
|
<source>LLM API Key</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">409</context>
|
<context context-type="linenumber">402</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6126617860376156501" datatype="html">
|
<trans-unit id="6126617860376156501" datatype="html">
|
||||||
<source>LLM Endpoint</source>
|
<source>LLM Endpoint</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">416</context>
|
<context context-type="linenumber">409</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6572826277249350975" datatype="html">
|
<trans-unit id="6572826277249350975" datatype="html">
|
||||||
<source>LLM Output Language</source>
|
<source>LLM Output Language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">423</context>
|
<context context-type="linenumber">416</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3284403507172415792" datatype="html">
|
<trans-unit id="3284403507172415792" datatype="html">
|
||||||
<source>Language to use for generated AI suggestions. When unset, AI suggestions use the user's display language if explicitly set.</source>
|
<source>Language to use for generated AI suggestions. When unset, AI suggestions use the user's display language if explicitly set.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">427</context>
|
<context context-type="linenumber">420</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4493921125434706859" datatype="html">
|
<trans-unit id="4493921125434706859" datatype="html">
|
||||||
<source>LLM Request Timeout</source>
|
<source>LLM Request Timeout</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">431</context>
|
<context context-type="linenumber">424</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="483994032066441287" datatype="html">
|
<trans-unit id="483994032066441287" datatype="html">
|
||||||
<source>Timeout in seconds for LLM requests.</source>
|
<source>Timeout in seconds for LLM requests.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">435</context>
|
<context context-type="linenumber">428</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1055686627716339120" datatype="html">
|
<trans-unit id="1055686627716339120" datatype="html">
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
||||||
<div class="list-group list-group-flush small pb-0">
|
<div class="list-group list-group-flush small pb-0">
|
||||||
@if (novelSuggestions === 0 && fieldSuggestions === 0) {
|
@if (novelSuggestions === 0 && reusableSuggestions === 0) {
|
||||||
<div class="list-group-item text-muted fst-italic">
|
<div class="list-group-item text-muted fst-italic">
|
||||||
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
|
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (fieldSuggestions > 0) {
|
@if (reusableSuggestions > 0) {
|
||||||
<div class="list-group-item text-muted fst-italic">
|
<div class="list-group-item text-muted fst-italic">
|
||||||
<small class="text-muted small fst-italic" i18n>{fieldSuggestions, plural, =1 {1 suggestion available below} other {{{fieldSuggestions}} suggestions available below}}</small>
|
<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>
|
||||||
|
|||||||
+4
-62
@@ -84,7 +84,7 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
|
|
||||||
expect(fixture.nativeElement.textContent).toContain(
|
expect(fixture.nativeElement.textContent).toContain(
|
||||||
'2 suggestions available below'
|
'2 existing values suggested below'
|
||||||
)
|
)
|
||||||
expect(fixture.nativeElement.textContent).not.toContain(
|
expect(fixture.nativeElement.textContent).not.toContain(
|
||||||
'No novel suggestions'
|
'No novel suggestions'
|
||||||
@@ -108,7 +108,7 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
expect(component.totalSuggestions).toBe(4)
|
expect(component.totalSuggestions).toBe(4)
|
||||||
expect(fixture.nativeElement.textContent).toContain('Arbitration')
|
expect(fixture.nativeElement.textContent).toContain('Arbitration')
|
||||||
expect(fixture.nativeElement.textContent).toContain(
|
expect(fixture.nativeElement.textContent).toContain(
|
||||||
'2 suggestions available below'
|
'2 existing values suggested below'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -125,46 +125,10 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(component.novelSuggestions).toBe(0)
|
expect(component.novelSuggestions).toBe(0)
|
||||||
expect(component.totalSuggestions).toBe(6)
|
expect(component.totalSuggestions).toBe(5)
|
||||||
|
|
||||||
fixture.componentRef.setInput('appliedStoragePath', 7)
|
fixture.componentRef.setInput('appliedStoragePath', 7)
|
||||||
expect(component.totalSuggestions).toBe(5)
|
expect(component.totalSuggestions).toBe(4)
|
||||||
})
|
|
||||||
|
|
||||||
it('should count title and dates as field suggestions without calling them existing values', () => {
|
|
||||||
fixture.componentRef.setInput('aiEnabled', true)
|
|
||||||
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
|
|
||||||
fixture.componentRef.setInput('suggestions', {
|
|
||||||
title: 'Suggested title',
|
|
||||||
dates: ['2026-01-04', '2026-02-01', '2026-03-01'],
|
|
||||||
correspondents: [1, 2, 3, 4],
|
|
||||||
document_types: [1, 2, 3, 4],
|
|
||||||
tags: [1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
})
|
|
||||||
fixture.detectChanges()
|
|
||||||
component.clickSuggest()
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
expect(component.reusableSuggestions).toBe(16)
|
|
||||||
expect(component.fieldSuggestions).toBe(20)
|
|
||||||
expect(component.totalSuggestions).toBe(20)
|
|
||||||
expect(fixture.nativeElement.textContent).toContain(
|
|
||||||
'20 suggestions available below'
|
|
||||||
)
|
|
||||||
expect(fixture.nativeElement.textContent).not.toContain('existing value')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not count title or date suggestions matching the current values', () => {
|
|
||||||
fixture.componentRef.setInput('suggestions', {
|
|
||||||
title: 'Current title',
|
|
||||||
dates: ['2026-01-04', '2026-02-01'],
|
|
||||||
})
|
|
||||||
expect(component.fieldSuggestions).toBe(3)
|
|
||||||
|
|
||||||
fixture.componentRef.setInput('appliedTitle', 'Current title')
|
|
||||||
fixture.componentRef.setInput('appliedCreated', '2026-01-04')
|
|
||||||
expect(component.fieldSuggestions).toBe(1)
|
|
||||||
expect(component.totalSuggestions).toBe(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show when a completed request returned no suggestions', () => {
|
it('should show when a completed request returned no suggestions', () => {
|
||||||
@@ -181,28 +145,6 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
expect(fixture.nativeElement.textContent).toContain('No suggestions')
|
expect(fixture.nativeElement.textContent).toContain('No suggestions')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should wait for all pending responses before showing the empty state', () => {
|
|
||||||
fixture.componentRef.setInput('aiEnabled', true)
|
|
||||||
fixture.componentRef.setInput('source', SuggestionSource.Both)
|
|
||||||
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
|
|
||||||
fixture.componentRef.setInput('suggestions', { tags: [] })
|
|
||||||
fixture.componentRef.setInput('loading', true)
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
expect(component.noSuggestions).toBeFalsy()
|
|
||||||
expect(fixture.nativeElement.textContent).not.toContain('No suggestions')
|
|
||||||
expect(
|
|
||||||
fixture.nativeElement.querySelector('[role="status"]')
|
|
||||||
).not.toBeNull()
|
|
||||||
|
|
||||||
fixture.componentRef.setInput('loading', false)
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
expect(component.noSuggestions).toBeTruthy()
|
|
||||||
expect(fixture.nativeElement.textContent).toContain('No suggestions')
|
|
||||||
expect(fixture.nativeElement.querySelector('[role="status"]')).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show the empty state before a request or with suggestions', () => {
|
it('should not show the empty state before a request or with suggestions', () => {
|
||||||
expect(component.noSuggestions).toBeFalsy()
|
expect(component.noSuggestions).toBeFalsy()
|
||||||
|
|
||||||
|
|||||||
+1
-25
@@ -34,8 +34,6 @@ export class SuggestionsDropdownComponent {
|
|||||||
readonly appliedCorrespondent = input<number>(null)
|
readonly appliedCorrespondent = input<number>(null)
|
||||||
readonly appliedDocumentType = input<number>(null)
|
readonly appliedDocumentType = input<number>(null)
|
||||||
readonly appliedStoragePath = input<number>(null)
|
readonly appliedStoragePath = input<number>(null)
|
||||||
readonly appliedTitle = input<string>(null)
|
|
||||||
readonly appliedCreated = input<string>(null)
|
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
getSuggestions: EventEmitter<SuggestionSource> = new EventEmitter()
|
getSuggestions: EventEmitter<SuggestionSource> = new EventEmitter()
|
||||||
@@ -130,28 +128,7 @@ export class SuggestionsDropdownComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get totalSuggestions(): number {
|
get totalSuggestions(): number {
|
||||||
return this.novelSuggestions + this.fieldSuggestions
|
return this.novelSuggestions + this.reusableSuggestions
|
||||||
}
|
|
||||||
|
|
||||||
get fieldSuggestions(): number {
|
|
||||||
return (
|
|
||||||
this.reusableSuggestions +
|
|
||||||
this.unappliedTitleSuggestions +
|
|
||||||
this.unappliedDateSuggestions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide a title or date suggestion equal to the current value
|
|
||||||
private get unappliedTitleSuggestions(): number {
|
|
||||||
const title = this.suggestions()?.title
|
|
||||||
return title && title !== this.appliedTitle() ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private get unappliedDateSuggestions(): number {
|
|
||||||
const created = this.appliedCreated()
|
|
||||||
return (this.suggestions()?.dates ?? []).filter(
|
|
||||||
(date) => !created || date !== created
|
|
||||||
).length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private countUnapplied(suggested: number[], applied: number[]): number {
|
private countUnapplied(suggested: number[], applied: number[]): number {
|
||||||
@@ -162,7 +139,6 @@ export class SuggestionsDropdownComponent {
|
|||||||
get noSuggestions(): boolean {
|
get noSuggestions(): boolean {
|
||||||
const suggestions = this.suggestions()
|
const suggestions = this.suggestions()
|
||||||
return (
|
return (
|
||||||
!this.loading() &&
|
|
||||||
suggestions != null &&
|
suggestions != null &&
|
||||||
!suggestions.title &&
|
!suggestions.title &&
|
||||||
!suggestions.tags?.length &&
|
!suggestions.tags?.length &&
|
||||||
|
|||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
<table class="table table-borderless align-baseline">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th i18n>Page</th>
|
|
||||||
<th i18n>Type</th>
|
|
||||||
<th i18n>Content</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (barcode of barcodes(); track $index) {
|
|
||||||
<tr>
|
|
||||||
<td>{{ barcode.page }}</td>
|
|
||||||
<td class="text-nowrap">{{ barcode.format }}</td>
|
|
||||||
<td class="text-break">
|
|
||||||
@if (isLink(barcode.value)) {
|
|
||||||
<a
|
|
||||||
[href]="barcode.value"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
>{{ barcode.value }}</a
|
|
||||||
>
|
|
||||||
} @else {
|
|
||||||
{{ barcode.value }}
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
(click)="copy($index)"
|
|
||||||
title="Copy content"
|
|
||||||
i18n-title
|
|
||||||
>
|
|
||||||
@if (copiedIndex() === $index) {
|
|
||||||
<i-bs name="clipboard-check"></i-bs>
|
|
||||||
} @else {
|
|
||||||
<i-bs name="clipboard"></i-bs>
|
|
||||||
}
|
|
||||||
<span class="visually-hidden" i18n>Copy content</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
import { Clipboard } from '@angular/cdk/clipboard'
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { DocumentBarcodesComponent } from './document-barcodes.component'
|
|
||||||
|
|
||||||
const barcodes = [
|
|
||||||
{ page: 1, value: 'ASN00123', format: 'Code128' },
|
|
||||||
{ page: 2, value: 'https://example.com/invoice/4711', format: 'QRCode' },
|
|
||||||
{ page: 2, value: 'javascript:alert(1)', format: 'QRCode' },
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('DocumentBarcodesComponent', () => {
|
|
||||||
let component: DocumentBarcodesComponent
|
|
||||||
let fixture: ComponentFixture<DocumentBarcodesComponent>
|
|
||||||
let clipboard: Clipboard
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
DocumentBarcodesComponent,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DocumentBarcodesComponent)
|
|
||||||
component = fixture.componentInstance
|
|
||||||
clipboard = TestBed.inject(Clipboard)
|
|
||||||
fixture.componentRef.setInput('barcodes', barcodes)
|
|
||||||
fixture.detectChanges()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should display all barcodes', () => {
|
|
||||||
const rows = fixture.nativeElement.querySelectorAll('tbody tr')
|
|
||||||
expect(rows).toHaveLength(3)
|
|
||||||
expect(rows[0].textContent).toContain('ASN00123')
|
|
||||||
expect(rows[0].textContent).toContain('Code128')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should only link http(s) values', () => {
|
|
||||||
const links = fixture.nativeElement.querySelectorAll('tbody a')
|
|
||||||
expect(links).toHaveLength(1)
|
|
||||||
expect(links[0].getAttribute('href')).toEqual(
|
|
||||||
'https://example.com/invoice/4711'
|
|
||||||
)
|
|
||||||
expect(links[0].getAttribute('target')).toEqual('_blank')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should copy a value and show feedback', () => {
|
|
||||||
jest.useFakeTimers()
|
|
||||||
const copySpy = jest.spyOn(clipboard, 'copy').mockReturnValue(true)
|
|
||||||
const buttons = fixture.nativeElement.querySelectorAll('tbody button')
|
|
||||||
buttons[0].click()
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(copySpy).toHaveBeenCalledWith('ASN00123')
|
|
||||||
expect(component.copiedIndex()).toEqual(0)
|
|
||||||
expect(buttons[0].querySelector('i-bs').getAttribute('name')).toEqual(
|
|
||||||
'clipboard-check'
|
|
||||||
)
|
|
||||||
jest.advanceTimersByTime(3000)
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(component.copiedIndex()).toBeNull()
|
|
||||||
expect(buttons[0].querySelector('i-bs').getAttribute('name')).toEqual(
|
|
||||||
'clipboard'
|
|
||||||
)
|
|
||||||
jest.useRealTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show feedback if copying failed', () => {
|
|
||||||
jest.spyOn(clipboard, 'copy').mockReturnValue(false)
|
|
||||||
component.copy(1)
|
|
||||||
expect(component.copiedIndex()).toBeNull()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
import { Clipboard } from '@angular/cdk/clipboard'
|
|
||||||
import { Component, inject, input, OnDestroy, signal } from '@angular/core'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { DocumentBarcode } from 'src/app/data/document-barcode'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'pngx-document-barcodes',
|
|
||||||
templateUrl: './document-barcodes.component.html',
|
|
||||||
imports: [NgxBootstrapIconsModule],
|
|
||||||
})
|
|
||||||
export class DocumentBarcodesComponent implements OnDestroy {
|
|
||||||
private readonly clipboard = inject(Clipboard)
|
|
||||||
|
|
||||||
readonly barcodes = input<DocumentBarcode[]>([])
|
|
||||||
|
|
||||||
readonly copiedIndex = signal<number>(null)
|
|
||||||
private copyTimeout: ReturnType<typeof setTimeout>
|
|
||||||
|
|
||||||
public isLink(value: string): boolean {
|
|
||||||
try {
|
|
||||||
const url = new URL(value.trim())
|
|
||||||
return ['http:', 'https:'].includes(url.protocol) && !!url.host
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public copy(index: number) {
|
|
||||||
if (!this.clipboard.copy(this.barcodes()[index].value)) return
|
|
||||||
this.copiedIndex.set(index)
|
|
||||||
clearTimeout(this.copyTimeout)
|
|
||||||
this.copyTimeout = setTimeout(() => this.copiedIndex.set(null), 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
clearTimeout(this.copyTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -141,8 +141,6 @@
|
|||||||
[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"
|
||||||
[appliedTitle]="documentForm.value.title"
|
|
||||||
[appliedCreated]="documentForm.value.created"
|
|
||||||
(getSuggestions)="getSuggestions($event)"
|
(getSuggestions)="getSuggestions($event)"
|
||||||
(sourceChange)="suggestionSourceOverride.set($event)"
|
(sourceChange)="suggestionSourceOverride.set($event)"
|
||||||
(addTag)="createTag($event)"
|
(addTag)="createTag($event)"
|
||||||
@@ -356,10 +354,6 @@
|
|||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (metadata()?.barcodes?.length > 0) {
|
|
||||||
<h6 i18n>Barcodes</h6>
|
|
||||||
<pngx-document-barcodes [barcodes]="metadata().barcodes"></pngx-document-barcodes>
|
|
||||||
}
|
|
||||||
@if (metadata()?.original_metadata?.length > 0) {
|
@if (metadata()?.original_metadata?.length > 0) {
|
||||||
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata()?.original_metadata"></pngx-metadata-collapse>
|
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata()?.original_metadata"></pngx-metadata-collapse>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -662,45 +662,6 @@ describe('DocumentDetailComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
|
||||||
['tag', 'createTag', 'tags', 'suggested_tags'],
|
|
||||||
[
|
|
||||||
'document type',
|
|
||||||
'createDocumentType',
|
|
||||||
'document_type',
|
|
||||||
'suggested_document_types',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'correspondent',
|
|
||||||
'createCorrespondent',
|
|
||||||
'correspondent',
|
|
||||||
'suggested_correspondents',
|
|
||||||
],
|
|
||||||
])(
|
|
||||||
'should create a %s after ML-only suggestions',
|
|
||||||
(_, method, field, suggestedField) => {
|
|
||||||
initNormally()
|
|
||||||
component.suggestions.set({ tags: [1] })
|
|
||||||
let openModal: NgbModalRef
|
|
||||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
|
||||||
component[method]('New value')
|
|
||||||
openModal.componentInstance.succeeded.next({
|
|
||||||
id: 12,
|
|
||||||
name: 'New value',
|
|
||||||
is_inbox_tag: false,
|
|
||||||
color: '#ff0000',
|
|
||||||
text_color: '#000000',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (field === 'tags') {
|
|
||||||
expect(component.tagsInput.value).toContain(12)
|
|
||||||
} else {
|
|
||||||
expect(component.documentForm.get(field).value).toBe(12)
|
|
||||||
}
|
|
||||||
expect(component.suggestions()[suggestedField]).toEqual([])
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
it('should support creating storage path', () => {
|
it('should support creating storage path', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
let openModal: NgbModalRef
|
let openModal: NgbModalRef
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-li
|
|||||||
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { DocumentBarcodesComponent } from './document-barcodes/document-barcodes.component'
|
|
||||||
import { DocumentHistoryComponent } from './document-history/document-history.component'
|
import { DocumentHistoryComponent } from './document-history/document-history.component'
|
||||||
import { DocumentVersionDropdownComponent } from './document-version-dropdown/document-version-dropdown.component'
|
import { DocumentVersionDropdownComponent } from './document-version-dropdown/document-version-dropdown.component'
|
||||||
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
||||||
@@ -178,7 +177,6 @@ interface IncomingDocumentUpdate {
|
|||||||
DateComponent,
|
DateComponent,
|
||||||
DocumentLinkComponent,
|
DocumentLinkComponent,
|
||||||
MetadataCollapseComponent,
|
MetadataCollapseComponent,
|
||||||
DocumentBarcodesComponent,
|
|
||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
TagsComponent,
|
TagsComponent,
|
||||||
@@ -1154,7 +1152,7 @@ export class DocumentDetailComponent
|
|||||||
if (this.suggestions()) {
|
if (this.suggestions()) {
|
||||||
this.suggestions.set({
|
this.suggestions.set({
|
||||||
...this.suggestions(),
|
...this.suggestions(),
|
||||||
suggested_tags: (this.suggestions().suggested_tags ?? []).filter(
|
suggested_tags: this.suggestions().suggested_tags.filter(
|
||||||
(tag) => tag !== newTag.name
|
(tag) => tag !== newTag.name
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -1193,12 +1191,10 @@ export class DocumentDetailComponent
|
|||||||
this.documentForm.get('document_type').setValue(newDocumentType.id)
|
this.documentForm.get('document_type').setValue(newDocumentType.id)
|
||||||
this.documentForm.get('document_type').markAsDirty()
|
this.documentForm.get('document_type').markAsDirty()
|
||||||
if (this.suggestions()) {
|
if (this.suggestions()) {
|
||||||
this.suggestions.set({
|
this.suggestions().suggested_document_types =
|
||||||
...this.suggestions(),
|
this.suggestions().suggested_document_types.filter(
|
||||||
suggested_document_types: (
|
(dt) => dt !== newName
|
||||||
this.suggestions().suggested_document_types ?? []
|
)
|
||||||
).filter((dt) => dt !== newName),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1225,12 +1221,10 @@ export class DocumentDetailComponent
|
|||||||
this.documentForm.get('correspondent').setValue(newCorrespondent.id)
|
this.documentForm.get('correspondent').setValue(newCorrespondent.id)
|
||||||
this.documentForm.get('correspondent').markAsDirty()
|
this.documentForm.get('correspondent').markAsDirty()
|
||||||
if (this.suggestions()) {
|
if (this.suggestions()) {
|
||||||
this.suggestions.set({
|
this.suggestions().suggested_correspondents =
|
||||||
...this.suggestions(),
|
this.suggestions().suggested_correspondents.filter(
|
||||||
suggested_correspondents: (
|
(c) => c !== newName
|
||||||
this.suggestions().suggested_correspondents ?? []
|
)
|
||||||
).filter((c) => c !== newName),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface DocumentBarcode {
|
|
||||||
page: number
|
|
||||||
|
|
||||||
value: string
|
|
||||||
|
|
||||||
format: string
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { DocumentBarcode } from './document-barcode'
|
|
||||||
|
|
||||||
export interface DocumentMetadata {
|
export interface DocumentMetadata {
|
||||||
original_checksum?: string
|
original_checksum?: string
|
||||||
|
|
||||||
@@ -14,6 +12,4 @@ export interface DocumentMetadata {
|
|||||||
has_archive_version?: boolean
|
has_archive_version?: boolean
|
||||||
|
|
||||||
lang?: string
|
lang?: string
|
||||||
|
|
||||||
barcodes?: DocumentBarcode[]
|
|
||||||
}
|
}
|
||||||
@@ -330,13 +330,6 @@ export const PaperlessConfigOptions: ConfigOption[] = [
|
|||||||
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT',
|
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT',
|
||||||
category: ConfigCategory.Barcode,
|
category: ConfigCategory.Barcode,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'barcode_store_values',
|
|
||||||
title: $localize`Store Barcode Contents`,
|
|
||||||
type: ConfigOptionType.Boolean,
|
|
||||||
config_key: 'PAPERLESS_CONSUMER_STORE_BARCODE_VALUES',
|
|
||||||
category: ConfigCategory.Barcode,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'ai_enabled',
|
key: 'ai_enabled',
|
||||||
title: $localize`AI Enabled`,
|
title: $localize`AI Enabled`,
|
||||||
@@ -465,7 +458,6 @@ export interface PaperlessConfig extends ObjectWithId {
|
|||||||
barcode_enable_tag: boolean
|
barcode_enable_tag: boolean
|
||||||
barcode_tag_mapping: object
|
barcode_tag_mapping: object
|
||||||
barcode_tag_split: boolean
|
barcode_tag_split: boolean
|
||||||
barcode_store_values: boolean
|
|
||||||
remote_ocr_engine: string
|
remote_ocr_engine: string
|
||||||
remote_ocr_api_key: string
|
remote_ocr_api_key: string
|
||||||
remote_ocr_endpoint: string
|
remote_ocr_endpoint: string
|
||||||
|
|||||||
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
+74
-126
@@ -18,7 +18,6 @@ from documents.converters import convert_from_tiff_to_pdf
|
|||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.data_models import StoredBarcode
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
@@ -48,7 +47,6 @@ class Barcode:
|
|||||||
page: int
|
page: int
|
||||||
value: str
|
value: str
|
||||||
settings: BarcodeConfig
|
settings: BarcodeConfig
|
||||||
format: str = ""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_separator(self) -> bool:
|
def is_separator(self) -> bool:
|
||||||
@@ -80,12 +78,6 @@ class Barcode:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stored(self) -> StoredBarcode:
|
|
||||||
"""
|
|
||||||
The barcode as it is stored with a document, page 1-indexed
|
|
||||||
"""
|
|
||||||
return {"page": self.page + 1, "value": self.value, "format": self.format}
|
|
||||||
|
|
||||||
|
|
||||||
class BarcodePlugin(ConsumeTaskPlugin):
|
class BarcodePlugin(ConsumeTaskPlugin):
|
||||||
NAME: str = "BarcodePlugin"
|
NAME: str = "BarcodePlugin"
|
||||||
@@ -97,12 +89,16 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
- ASN from barcode detection is enabled or
|
- ASN from barcode detection is enabled or
|
||||||
- Barcode support is enabled and the mime type is supported
|
- Barcode support is enabled and the mime type is supported
|
||||||
"""
|
"""
|
||||||
|
if self.settings.barcode_enable_tiff_support:
|
||||||
|
supported_mimes: set[str] = {"application/pdf", "image/tiff"}
|
||||||
|
else:
|
||||||
|
supported_mimes = {"application/pdf"}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
self.settings.barcode_enable_asn
|
self.settings.barcode_enable_asn
|
||||||
or self.settings.barcodes_enabled
|
or self.settings.barcodes_enabled
|
||||||
or self.settings.barcode_enable_tag
|
or self.settings.barcode_enable_tag
|
||||||
or self.settings.barcode_store_values
|
) and self.input_doc.mime_type in supported_mimes
|
||||||
) and self.input_doc.mime_type in scannable_mime_types(self.settings)
|
|
||||||
|
|
||||||
def get_settings(self) -> BarcodeConfig:
|
def get_settings(self) -> BarcodeConfig:
|
||||||
"""
|
"""
|
||||||
@@ -248,10 +244,6 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
|
if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
|
||||||
self._apply_detected_asn(located_asn)
|
self._apply_detected_asn(located_asn)
|
||||||
|
|
||||||
# After splitting too, so each split document keeps its own barcodes
|
|
||||||
if self.settings.barcode_store_values:
|
|
||||||
self.metadata.barcodes = [x.stored() for x in self.barcodes] or None
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
self.temp_dir.cleanup()
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
@@ -270,6 +262,22 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
)
|
)
|
||||||
self._tiff_conversion_done = True
|
self._tiff_conversion_done = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_barcodes_zxing(image: Image.Image) -> list[str]:
|
||||||
|
barcodes = []
|
||||||
|
|
||||||
|
import zxingcpp
|
||||||
|
|
||||||
|
detected_barcodes = zxingcpp.read_barcodes(image)
|
||||||
|
for barcode in detected_barcodes:
|
||||||
|
if barcode.text:
|
||||||
|
barcodes.append(barcode.text)
|
||||||
|
logger.debug(
|
||||||
|
f"Barcode of type {barcode.format} found: {barcode.text}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return barcodes
|
||||||
|
|
||||||
def detect(self) -> None:
|
def detect(self) -> None:
|
||||||
"""
|
"""
|
||||||
Scan all pages of the PDF as images, updating barcodes and the pages
|
Scan all pages of the PDF as images, updating barcodes and the pages
|
||||||
@@ -283,12 +291,60 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
self.convert_from_tiff_to_pdf()
|
self.convert_from_tiff_to_pdf()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.barcodes = scan_pdf(
|
# Read number of pages from pdf
|
||||||
self.pdf_file,
|
with Pdf.open(self.pdf_file) as pdf:
|
||||||
self.settings,
|
num_of_pages = len(pdf.pages)
|
||||||
Path(self.temp_dir.name),
|
logger.debug(f"PDF has {num_of_pages} pages")
|
||||||
|
|
||||||
|
# Get limit from configuration
|
||||||
|
barcode_max_pages: int = (
|
||||||
|
num_of_pages
|
||||||
|
if self.settings.barcode_max_pages == 0
|
||||||
|
else self.settings.barcode_max_pages
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if barcode_max_pages < num_of_pages: # pragma: no cover
|
||||||
|
logger.debug(
|
||||||
|
f"Barcodes detection will be limited to the first {barcode_max_pages} pages",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loop al page
|
||||||
|
for current_page_number in range(min(num_of_pages, barcode_max_pages)):
|
||||||
|
logger.debug(f"Processing page {current_page_number}")
|
||||||
|
|
||||||
|
# Convert page to image
|
||||||
|
page = convert_from_path(
|
||||||
|
self.pdf_file,
|
||||||
|
dpi=self.settings.barcode_dpi,
|
||||||
|
output_folder=self.temp_dir.name,
|
||||||
|
first_page=current_page_number + 1,
|
||||||
|
last_page=current_page_number + 1,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
# Remember filename, since it is lost by upscaling
|
||||||
|
page_filepath = Path(page.filename)
|
||||||
|
logger.debug(f"Image is at {page_filepath}")
|
||||||
|
|
||||||
|
# Upscale image if configured
|
||||||
|
factor = self.settings.barcode_upscale
|
||||||
|
if factor > 1.0:
|
||||||
|
logger.debug(
|
||||||
|
f"Upscaling image by {factor} for better barcode detection",
|
||||||
|
)
|
||||||
|
x, y = page.size
|
||||||
|
page = page.resize(
|
||||||
|
(round(x * factor), (round(y * factor))),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Detect barcodes
|
||||||
|
for barcode_value in self.read_barcodes_zxing(page):
|
||||||
|
self.barcodes.append(
|
||||||
|
Barcode(current_page_number, barcode_value, self.settings),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete temporary image file
|
||||||
|
page_filepath.unlink()
|
||||||
|
|
||||||
# Password protected files can't be checked
|
# Password protected files can't be checked
|
||||||
# This is the exception raised for those
|
# This is the exception raised for those
|
||||||
except PasswordError as e:
|
except PasswordError as e:
|
||||||
@@ -478,111 +534,3 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
document_paths.append(savepath)
|
document_paths.append(savepath)
|
||||||
|
|
||||||
return document_paths
|
return document_paths
|
||||||
|
|
||||||
|
|
||||||
def scannable_mime_types(settings: BarcodeConfig) -> set[str]:
|
|
||||||
"""
|
|
||||||
The file types the barcode scan supports with the current settings
|
|
||||||
"""
|
|
||||||
if settings.barcode_enable_tiff_support:
|
|
||||||
return {"application/pdf", "image/tiff"}
|
|
||||||
return {"application/pdf"}
|
|
||||||
|
|
||||||
|
|
||||||
def read_barcodes_zxing(image: Image.Image) -> list[tuple[str, str]]:
|
|
||||||
"""
|
|
||||||
Returns the text and format (zxing enum name) of each barcode found in
|
|
||||||
the image
|
|
||||||
"""
|
|
||||||
barcodes = []
|
|
||||||
|
|
||||||
import zxingcpp
|
|
||||||
|
|
||||||
detected_barcodes = zxingcpp.read_barcodes(image)
|
|
||||||
for barcode in detected_barcodes:
|
|
||||||
if barcode.text:
|
|
||||||
barcodes.append((barcode.text, barcode.format.name))
|
|
||||||
logger.debug(
|
|
||||||
f"Barcode of type {barcode.format} found: {barcode.text}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return barcodes
|
|
||||||
|
|
||||||
|
|
||||||
def scan_pdf(pdf_path: Path, settings: BarcodeConfig, work_dir: Path) -> list[Barcode]:
|
|
||||||
"""
|
|
||||||
Scans the pages of a PDF as images for barcodes. Errors are not caught,
|
|
||||||
so callers can tell a failed scan from one that found nothing.
|
|
||||||
"""
|
|
||||||
barcodes: list[Barcode] = []
|
|
||||||
|
|
||||||
with Pdf.open(pdf_path) as pdf:
|
|
||||||
num_of_pages = len(pdf.pages)
|
|
||||||
logger.debug(f"PDF has {num_of_pages} pages")
|
|
||||||
|
|
||||||
# Get limit from configuration
|
|
||||||
barcode_max_pages: int = (
|
|
||||||
num_of_pages if settings.barcode_max_pages == 0 else settings.barcode_max_pages
|
|
||||||
)
|
|
||||||
|
|
||||||
if barcode_max_pages < num_of_pages: # pragma: no cover
|
|
||||||
logger.debug(
|
|
||||||
f"Barcodes detection will be limited to the first {barcode_max_pages} pages",
|
|
||||||
)
|
|
||||||
|
|
||||||
for current_page_number in range(min(num_of_pages, barcode_max_pages)):
|
|
||||||
logger.debug(f"Processing page {current_page_number}")
|
|
||||||
|
|
||||||
# Convert page to image
|
|
||||||
page = convert_from_path(
|
|
||||||
pdf_path,
|
|
||||||
dpi=settings.barcode_dpi,
|
|
||||||
output_folder=work_dir,
|
|
||||||
first_page=current_page_number + 1,
|
|
||||||
last_page=current_page_number + 1,
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# Remember filename, since it is lost by upscaling
|
|
||||||
page_filepath = Path(page.filename)
|
|
||||||
logger.debug(f"Image is at {page_filepath}")
|
|
||||||
|
|
||||||
# Upscale image if configured
|
|
||||||
factor = settings.barcode_upscale
|
|
||||||
if factor > 1.0:
|
|
||||||
logger.debug(
|
|
||||||
f"Upscaling image by {factor} for better barcode detection",
|
|
||||||
)
|
|
||||||
x, y = page.size
|
|
||||||
page = page.resize(
|
|
||||||
(round(x * factor), (round(y * factor))),
|
|
||||||
)
|
|
||||||
|
|
||||||
for barcode_value, barcode_format in read_barcodes_zxing(page):
|
|
||||||
barcodes.append(
|
|
||||||
Barcode(current_page_number, barcode_value, settings, barcode_format),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete temporary image file
|
|
||||||
page_filepath.unlink()
|
|
||||||
|
|
||||||
return barcodes
|
|
||||||
|
|
||||||
|
|
||||||
def read_barcode_values(
|
|
||||||
path: Path,
|
|
||||||
mime_type: str,
|
|
||||||
settings: BarcodeConfig,
|
|
||||||
work_dir: Path,
|
|
||||||
) -> list[StoredBarcode] | None:
|
|
||||||
"""
|
|
||||||
Reads the barcodes of a file outside of the consumption plugins: for new
|
|
||||||
versions, which skip the barcode plugin, and when reprocessing.
|
|
||||||
|
|
||||||
Returns None if the file can't be scanned with the current settings.
|
|
||||||
Errors while scanning are raised.
|
|
||||||
"""
|
|
||||||
if mime_type not in scannable_mime_types(settings):
|
|
||||||
return None
|
|
||||||
if mime_type == "image/tiff":
|
|
||||||
path = convert_from_tiff_to_pdf(path, work_dir)
|
|
||||||
return [x.stored() for x in scan_pdf(path, settings, work_dir)]
|
|
||||||
+185
-219
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
@@ -17,6 +18,7 @@ from django.db.models import Max
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from documents import pdf_ops
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
@@ -116,6 +118,11 @@ def _resolve_root_and_source_doc(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _scratch_path(name: str) -> Path:
|
||||||
|
"""A path inside a fresh directory under SCRATCH_DIR."""
|
||||||
|
return Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) / name
|
||||||
|
|
||||||
|
|
||||||
def set_correspondent(
|
def set_correspondent(
|
||||||
doc_ids: list[int],
|
doc_ids: list[int],
|
||||||
correspondent: Correspondent,
|
correspondent: Correspondent,
|
||||||
@@ -474,8 +481,6 @@ def rotate(
|
|||||||
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
||||||
docs_by_root_id.setdefault(pair.root_doc.id, pair)
|
docs_by_root_id.setdefault(pair.root_doc.id, pair)
|
||||||
|
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
for pair in docs_by_root_id.values():
|
for pair in docs_by_root_id.values():
|
||||||
if pair.source_doc.mime_type != "application/pdf":
|
if pair.source_doc.mime_type != "application/pdf":
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -488,11 +493,7 @@ def rotate(
|
|||||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||||
/ f"{pair.root_doc.id}_rotated.pdf"
|
/ f"{pair.root_doc.id}_rotated.pdf"
|
||||||
)
|
)
|
||||||
with pikepdf.open(pair.source_doc.source_path) as pdf:
|
pdf_ops.rotate_pdf(pair.source_doc.source_path, filepath, degrees)
|
||||||
for page in pdf.pages:
|
|
||||||
page.rotate(degrees, relative=True)
|
|
||||||
pdf.remove_unreferenced_resources()
|
|
||||||
pdf.save(filepath)
|
|
||||||
|
|
||||||
# Preserve metadata/permissions via overrides; mark as new version
|
# Preserve metadata/permissions via overrides; mark as new version
|
||||||
overrides = DocumentMetadataOverrides().from_document(pair.root_doc)
|
overrides = DocumentMetadataOverrides().from_document(pair.root_doc)
|
||||||
@@ -535,48 +536,45 @@ def merge(
|
|||||||
qs = Document.objects.select_related("root_document").filter(id__in=doc_ids)
|
qs = Document.objects.select_related("root_document").filter(id__in=doc_ids)
|
||||||
docs_by_id = {doc.id: doc for doc in qs}
|
docs_by_id = {doc.id: doc for doc in qs}
|
||||||
affected_docs: list[int] = []
|
affected_docs: list[int] = []
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
merged_pdf = pikepdf.new()
|
|
||||||
version: str = merged_pdf.pdf_version
|
|
||||||
handoff_asn: int | None = None
|
handoff_asn: int | None = None
|
||||||
# use doc_ids to preserve order
|
with pdf_ops.PdfMerger() as merger:
|
||||||
for doc_id in doc_ids:
|
# use doc_ids to preserve order
|
||||||
doc = docs_by_id.get(doc_id)
|
for doc_id in doc_ids:
|
||||||
if doc is None:
|
doc = docs_by_id.get(doc_id)
|
||||||
continue
|
if doc is None:
|
||||||
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
continue
|
||||||
try:
|
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
||||||
doc_path = (
|
try:
|
||||||
pair.source_doc.archive_path
|
# archive_path is None when there is no archive version
|
||||||
if archive_fallback
|
archive_path = (
|
||||||
and pair.source_doc.mime_type != "application/pdf"
|
pair.source_doc.archive_path
|
||||||
and pair.source_doc.has_archive_version
|
if archive_fallback
|
||||||
else pair.source_doc.source_path
|
and pair.source_doc.mime_type != "application/pdf"
|
||||||
)
|
else None
|
||||||
with pikepdf.open(str(doc_path)) as pdf:
|
)
|
||||||
version = max(version, pdf.pdf_version)
|
merger.add(
|
||||||
merged_pdf.pages.extend(pdf.pages)
|
archive_path
|
||||||
affected_docs.append(doc.id)
|
if archive_path is not None
|
||||||
if handoff_asn is None and doc.archive_serial_number is not None:
|
else pair.source_doc.source_path,
|
||||||
handoff_asn = doc.archive_serial_number
|
)
|
||||||
except Exception as e:
|
affected_docs.append(doc.id)
|
||||||
logger.exception(
|
if handoff_asn is None and doc.archive_serial_number is not None:
|
||||||
f"Error merging document {doc.id}, it will not be included in the merge: {e}",
|
handoff_asn = doc.archive_serial_number
|
||||||
)
|
except Exception as e:
|
||||||
if len(affected_docs) == 0:
|
logger.exception(
|
||||||
logger.warning("No documents were merged")
|
f"Error merging document {doc.id}, it will not be included in the merge: {e}",
|
||||||
return "OK"
|
)
|
||||||
|
if len(affected_docs) == 0:
|
||||||
|
logger.warning("No documents were merged")
|
||||||
|
return "OK"
|
||||||
|
|
||||||
filepath = (
|
filepath = (
|
||||||
Path(
|
Path(
|
||||||
tempfile.mkdtemp(dir=settings.SCRATCH_DIR),
|
tempfile.mkdtemp(dir=settings.SCRATCH_DIR),
|
||||||
|
)
|
||||||
|
/ f"{'_'.join([str(doc_id) for doc_id in affected_docs])[:100]}_merged.pdf"
|
||||||
)
|
)
|
||||||
/ f"{'_'.join([str(doc_id) for doc_id in affected_docs])[:100]}_merged.pdf"
|
merger.save(filepath)
|
||||||
)
|
|
||||||
merged_pdf.remove_unreferenced_resources()
|
|
||||||
merged_pdf.save(filepath, min_version=version)
|
|
||||||
merged_pdf.close()
|
|
||||||
|
|
||||||
if metadata_document_id:
|
if metadata_document_id:
|
||||||
metadata_document = qs.get(id=metadata_document_id)
|
metadata_document = qs.get(id=metadata_document_id)
|
||||||
@@ -752,64 +750,60 @@ def split(
|
|||||||
)
|
)
|
||||||
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
|
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
|
||||||
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
consume_tasks = []
|
consume_tasks = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with pikepdf.open(pair.source_doc.source_path) as pdf:
|
outputs = [
|
||||||
for idx, split_doc in enumerate(pages):
|
(
|
||||||
dst: pikepdf.Pdf = pikepdf.new()
|
[pdf_ops.PageSpec(page) for page in split_doc],
|
||||||
for page in split_doc:
|
partial(_scratch_path, f"{doc.id}_{split_doc[0]}-{split_doc[-1]}.pdf"),
|
||||||
dst.pages.append(pdf.pages[page - 1])
|
)
|
||||||
filepath: Path = (
|
for split_doc in pages
|
||||||
Path(
|
]
|
||||||
tempfile.mkdtemp(dir=settings.SCRATCH_DIR),
|
filepaths = pdf_ops.build_pdfs(pair.source_doc.source_path, outputs)
|
||||||
)
|
|
||||||
/ f"{doc.id}_{split_doc[0]}-{split_doc[-1]}.pdf"
|
|
||||||
)
|
|
||||||
dst.remove_unreferenced_resources()
|
|
||||||
dst.save(filepath)
|
|
||||||
dst.close()
|
|
||||||
|
|
||||||
overrides: DocumentMetadataOverrides = (
|
for idx, (split_doc, filepath) in enumerate(
|
||||||
DocumentMetadataOverrides().from_document(doc)
|
zip(pages, filepaths, strict=True),
|
||||||
)
|
):
|
||||||
overrides.title = f"{doc.title} (split {idx + 1})"
|
overrides: DocumentMetadataOverrides = (
|
||||||
if user is not None:
|
DocumentMetadataOverrides().from_document(doc)
|
||||||
overrides.owner_id = user.id
|
)
|
||||||
if not delete_originals:
|
overrides.title = f"{doc.title} (split {idx + 1})"
|
||||||
overrides.skip_asn_if_exists = True
|
if user is not None:
|
||||||
logger.info(
|
overrides.owner_id = user.id
|
||||||
f"Adding split document with pages {split_doc} to the task queue.",
|
if not delete_originals:
|
||||||
)
|
overrides.skip_asn_if_exists = True
|
||||||
consume_tasks.append(
|
logger.info(
|
||||||
consume_file.s(
|
f"Adding split document with pages {split_doc} to the task queue.",
|
||||||
input_doc=ConsumableDocument(
|
)
|
||||||
source=DocumentSource.ConsumeFolder,
|
consume_tasks.append(
|
||||||
original_file=filepath,
|
consume_file.s(
|
||||||
),
|
input_doc=ConsumableDocument(
|
||||||
overrides=overrides,
|
source=DocumentSource.ConsumeFolder,
|
||||||
).set(headers={"trigger_source": trigger_source}),
|
original_file=filepath,
|
||||||
)
|
),
|
||||||
|
overrides=overrides,
|
||||||
|
).set(headers={"trigger_source": trigger_source}),
|
||||||
|
)
|
||||||
|
|
||||||
if delete_originals:
|
if delete_originals:
|
||||||
backup = release_archive_serial_numbers([doc.id])
|
backup = release_archive_serial_numbers([doc.id])
|
||||||
logger.info(
|
logger.info(
|
||||||
"Queueing removal of original document after consumption of the split documents",
|
"Queueing removal of original document after consumption of the split documents",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
chord(
|
chord(
|
||||||
header=consume_tasks,
|
header=consume_tasks,
|
||||||
body=delete.si([doc.id]),
|
body=delete.si([doc.id]),
|
||||||
).on_error(
|
).on_error(
|
||||||
restore_archive_serial_numbers_task.s(backup),
|
restore_archive_serial_numbers_task.s(backup),
|
||||||
).apply_async()
|
).apply_async()
|
||||||
except Exception:
|
except Exception:
|
||||||
restore_archive_serial_numbers(backup)
|
restore_archive_serial_numbers(backup)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
group(consume_tasks).delay()
|
group(consume_tasks).delay()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error splitting document {doc.id}: {e}")
|
logger.exception(f"Error splitting document {doc.id}: {e}")
|
||||||
@@ -830,8 +824,7 @@ def delete_pages(
|
|||||||
)
|
)
|
||||||
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
|
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
|
||||||
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
||||||
pages = sorted(pages) # sort pages to avoid index issues
|
pages = sorted(set(pages))
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Produce edited PDF to a temp file and create a new version
|
# Produce edited PDF to a temp file and create a new version
|
||||||
@@ -839,13 +832,7 @@ def delete_pages(
|
|||||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||||
/ f"{pair.root_doc.id}_pages_deleted.pdf"
|
/ f"{pair.root_doc.id}_pages_deleted.pdf"
|
||||||
)
|
)
|
||||||
with pikepdf.open(pair.source_doc.source_path) as pdf:
|
pdf_ops.remove_pages(pair.source_doc.source_path, filepath, pages)
|
||||||
offset = 1 # pages are 1-indexed
|
|
||||||
for page_num in pages:
|
|
||||||
pdf.pages.remove(pdf.pages[page_num - offset])
|
|
||||||
offset += 1 # remove() changes the index of the pages
|
|
||||||
pdf.remove_unreferenced_resources()
|
|
||||||
pdf.save(filepath)
|
|
||||||
|
|
||||||
overrides = DocumentMetadataOverrides().from_document(pair.root_doc)
|
overrides = DocumentMetadataOverrides().from_document(pair.root_doc)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
@@ -894,47 +881,28 @@ def edit_pdf(
|
|||||||
)
|
)
|
||||||
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
|
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
|
||||||
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
pdf_docs: list[pikepdf.Pdf] = []
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not operations:
|
output_count = pdf_ops.validate_page_operations(
|
||||||
raise ValueError("Output document index is out of bounds")
|
operations,
|
||||||
|
single_output=update_document,
|
||||||
max_idx = max(op.get("doc", 0) for op in operations)
|
)
|
||||||
if update_document and max_idx > 0:
|
page_specs: list[list[pdf_ops.PageSpec]] = [[] for _ in range(output_count)]
|
||||||
logger.error(
|
for op in operations:
|
||||||
"Update requested but multiple output documents specified",
|
page_specs[op.get("doc", 0)].append(
|
||||||
|
pdf_ops.PageSpec(op["page"], op.get("rotate", 0)),
|
||||||
)
|
)
|
||||||
raise ValueError("Multiple output documents specified")
|
|
||||||
|
|
||||||
if any(
|
|
||||||
op.get("doc", 0) < 0 or op.get("doc", 0) >= len(operations)
|
|
||||||
for op in operations
|
|
||||||
):
|
|
||||||
raise ValueError("Output document index is out of bounds")
|
|
||||||
|
|
||||||
with pikepdf.open(pair.source_doc.source_path) as src:
|
|
||||||
# prepare output documents
|
|
||||||
pdf_docs = [pikepdf.new() for _ in range(max_idx + 1)]
|
|
||||||
|
|
||||||
for op in operations:
|
|
||||||
dst = pdf_docs[op.get("doc", 0)]
|
|
||||||
page = src.pages[op["page"] - 1]
|
|
||||||
dst.pages.append(page)
|
|
||||||
if op.get("rotate"):
|
|
||||||
dst.pages[-1].rotate(op["rotate"], relative=True)
|
|
||||||
|
|
||||||
if update_document:
|
if update_document:
|
||||||
# Create a new version from the edited PDF rather than replacing in-place
|
# Create a new version from the edited PDF rather than replacing in-place
|
||||||
pdf = pdf_docs[0]
|
(filepath,) = pdf_ops.build_pdfs(
|
||||||
pdf.remove_unreferenced_resources()
|
pair.source_doc.source_path,
|
||||||
filepath: Path = (
|
[
|
||||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
(
|
||||||
/ f"{pair.root_doc.id}_edited.pdf"
|
page_specs[0],
|
||||||
|
partial(_scratch_path, f"{pair.root_doc.id}_edited.pdf"),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
pdf.save(filepath)
|
|
||||||
overrides = (
|
overrides = (
|
||||||
DocumentMetadataOverrides().from_document(pair.root_doc)
|
DocumentMetadataOverrides().from_document(pair.root_doc)
|
||||||
if include_metadata
|
if include_metadata
|
||||||
@@ -955,6 +923,19 @@ def edit_pdf(
|
|||||||
headers={"trigger_source": trigger_source},
|
headers={"trigger_source": trigger_source},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
version_filepaths = pdf_ops.build_pdfs(
|
||||||
|
pair.source_doc.source_path,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
specs,
|
||||||
|
partial(
|
||||||
|
_scratch_path,
|
||||||
|
f"{pair.root_doc.id}_edit_{idx}.pdf",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for idx, specs in enumerate(page_specs, start=1)
|
||||||
|
],
|
||||||
|
)
|
||||||
consume_tasks = []
|
consume_tasks = []
|
||||||
overrides = (
|
overrides = (
|
||||||
DocumentMetadataOverrides().from_document(pair.root_doc)
|
DocumentMetadataOverrides().from_document(pair.root_doc)
|
||||||
@@ -966,15 +947,9 @@ def edit_pdf(
|
|||||||
overrides.actor_id = user.id
|
overrides.actor_id = user.id
|
||||||
if not delete_original:
|
if not delete_original:
|
||||||
overrides.skip_asn_if_exists = True
|
overrides.skip_asn_if_exists = True
|
||||||
if delete_original and len(pdf_docs) == 1:
|
if delete_original and output_count == 1:
|
||||||
overrides.asn = pair.root_doc.archive_serial_number
|
overrides.asn = pair.root_doc.archive_serial_number
|
||||||
for idx, pdf in enumerate(pdf_docs, start=1):
|
for version_filepath in version_filepaths:
|
||||||
version_filepath: Path = (
|
|
||||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
|
||||||
/ f"{pair.root_doc.id}_edit_{idx}.pdf"
|
|
||||||
)
|
|
||||||
pdf.remove_unreferenced_resources()
|
|
||||||
pdf.save(version_filepath)
|
|
||||||
consume_tasks.append(
|
consume_tasks.append(
|
||||||
consume_file.s(
|
consume_file.s(
|
||||||
input_doc=ConsumableDocument(
|
input_doc=ConsumableDocument(
|
||||||
@@ -1024,8 +999,6 @@ def remove_password(
|
|||||||
"""
|
"""
|
||||||
Remove password protection from PDF documents.
|
Remove password protection from PDF documents.
|
||||||
"""
|
"""
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
for doc_id in doc_ids:
|
for doc_id in doc_ids:
|
||||||
doc = Document.objects.select_related("root_document").get(id=doc_id)
|
doc = Document.objects.select_related("root_document").get(id=doc_id)
|
||||||
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
|
||||||
@@ -1039,76 +1012,69 @@ def remove_password(
|
|||||||
doc.id,
|
doc.id,
|
||||||
pair.source_doc.source_path,
|
pair.source_doc.source_path,
|
||||||
)
|
)
|
||||||
try:
|
if not pdf_ops.needs_decrypt(source_path):
|
||||||
with pikepdf.open(source_path) as pdf:
|
logger.info(
|
||||||
if not pdf.is_encrypted:
|
"Skipping password removal for document %s because the "
|
||||||
logger.info(
|
"source PDF is not encrypted",
|
||||||
"Skipping password removal for document %s because the "
|
pair.root_doc.id,
|
||||||
"source PDF is not encrypted",
|
|
||||||
pair.root_doc.id,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
except pikepdf.PasswordError:
|
|
||||||
# Password-protected PDFs need the supplied password below.
|
|
||||||
pass
|
|
||||||
|
|
||||||
with pikepdf.open(source_path, password=password) as pdf:
|
|
||||||
filepath: Path = (
|
|
||||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
|
||||||
/ f"{pair.root_doc.id}_unprotected.pdf"
|
|
||||||
)
|
)
|
||||||
pdf.remove_unreferenced_resources()
|
continue
|
||||||
pdf.save(filepath)
|
|
||||||
|
|
||||||
if update_document:
|
filepath = pdf_ops.decrypt_pdf(
|
||||||
# Create a new version rather than modifying the root/original in place.
|
source_path,
|
||||||
overrides = (
|
partial(_scratch_path, f"{pair.root_doc.id}_unprotected.pdf"),
|
||||||
DocumentMetadataOverrides().from_document(pair.root_doc)
|
password,
|
||||||
if include_metadata
|
)
|
||||||
else DocumentMetadataOverrides()
|
|
||||||
)
|
if update_document:
|
||||||
if user is not None:
|
# Create a new version rather than modifying the root/original in place.
|
||||||
overrides.owner_id = user.id
|
overrides = (
|
||||||
overrides.actor_id = user.id
|
DocumentMetadataOverrides().from_document(pair.root_doc)
|
||||||
consume_file.apply_async(
|
if include_metadata
|
||||||
kwargs={
|
else DocumentMetadataOverrides()
|
||||||
"input_doc": ConsumableDocument(
|
)
|
||||||
source=DocumentSource.ConsumeFolder,
|
if user is not None:
|
||||||
original_file=filepath,
|
overrides.owner_id = user.id
|
||||||
root_document_id=pair.root_doc.id,
|
overrides.actor_id = user.id
|
||||||
),
|
consume_file.apply_async(
|
||||||
"overrides": overrides,
|
kwargs={
|
||||||
},
|
"input_doc": ConsumableDocument(
|
||||||
headers={"trigger_source": trigger_source},
|
source=DocumentSource.ConsumeFolder,
|
||||||
)
|
original_file=filepath,
|
||||||
|
root_document_id=pair.root_doc.id,
|
||||||
|
),
|
||||||
|
"overrides": overrides,
|
||||||
|
},
|
||||||
|
headers={"trigger_source": trigger_source},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
consume_tasks = []
|
||||||
|
overrides = (
|
||||||
|
DocumentMetadataOverrides().from_document(pair.root_doc)
|
||||||
|
if include_metadata
|
||||||
|
else DocumentMetadataOverrides()
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
overrides.owner_id = user.id
|
||||||
|
overrides.actor_id = user.id
|
||||||
|
|
||||||
|
consume_tasks.append(
|
||||||
|
consume_file.s(
|
||||||
|
input_doc=ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=filepath,
|
||||||
|
),
|
||||||
|
overrides=overrides,
|
||||||
|
).set(headers={"trigger_source": trigger_source}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if delete_original:
|
||||||
|
chord(
|
||||||
|
header=consume_tasks,
|
||||||
|
body=delete.si([doc.id]),
|
||||||
|
).delay()
|
||||||
else:
|
else:
|
||||||
consume_tasks = []
|
group(consume_tasks).delay()
|
||||||
overrides = (
|
|
||||||
DocumentMetadataOverrides().from_document(pair.root_doc)
|
|
||||||
if include_metadata
|
|
||||||
else DocumentMetadataOverrides()
|
|
||||||
)
|
|
||||||
if user is not None:
|
|
||||||
overrides.owner_id = user.id
|
|
||||||
overrides.actor_id = user.id
|
|
||||||
|
|
||||||
consume_tasks.append(
|
|
||||||
consume_file.s(
|
|
||||||
input_doc=ConsumableDocument(
|
|
||||||
source=DocumentSource.ConsumeFolder,
|
|
||||||
original_file=filepath,
|
|
||||||
),
|
|
||||||
overrides=overrides,
|
|
||||||
).set(headers={"trigger_source": trigger_source}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if delete_original:
|
|
||||||
chord(
|
|
||||||
header=consume_tasks,
|
|
||||||
body=delete.si([doc.id]),
|
|
||||||
).delay()
|
|
||||||
else:
|
|
||||||
group(consume_tasks).delay()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from django.utils import timezone
|
|||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from documents.barcodes import read_barcode_values
|
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import ConsumeFileSuccessResult
|
from documents.data_models import ConsumeFileSuccessResult
|
||||||
@@ -32,7 +31,6 @@ from documents.models import Correspondent
|
|||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentBarcode
|
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
@@ -55,7 +53,6 @@ from documents.utils import compute_checksum
|
|||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
from documents.utils import run_subprocess
|
from documents.utils import run_subprocess
|
||||||
from paperless.config import BarcodeConfig
|
|
||||||
from paperless.config import OcrConfig
|
from paperless.config import OcrConfig
|
||||||
from paperless.config import RemoteOCRConfig
|
from paperless.config import RemoteOCRConfig
|
||||||
from paperless.models import ArchiveFileGenerationChoices
|
from paperless.models import ArchiveFileGenerationChoices
|
||||||
@@ -507,13 +504,6 @@ class ConsumerPlugin(
|
|||||||
f"Parser: {document_parser.name} v{document_parser.version}",
|
f"Parser: {document_parser.name} v{document_parser.version}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# New versions skip the barcode plugin, so read their barcodes here
|
|
||||||
if (
|
|
||||||
self.input_doc.root_document_id is not None
|
|
||||||
and self.metadata.barcodes is None
|
|
||||||
):
|
|
||||||
self._read_version_barcodes(mime_type, Path(tmpdir))
|
|
||||||
|
|
||||||
# Parse the document. This may take some time.
|
# Parse the document. This may take some time.
|
||||||
|
|
||||||
text = None
|
text = None
|
||||||
@@ -641,8 +631,6 @@ class ConsumerPlugin(
|
|||||||
else:
|
else:
|
||||||
original_document.save()
|
original_document.save()
|
||||||
|
|
||||||
self._store_barcodes(original_document)
|
|
||||||
|
|
||||||
# Adding a version changes the effective document, so update root modified
|
# Adding a version changes the effective document, so update root modified
|
||||||
Document.objects.filter(pk=root_doc.pk).update(
|
Document.objects.filter(pk=root_doc.pk).update(
|
||||||
modified=timezone.now(),
|
modified=timezone.now(),
|
||||||
@@ -974,32 +962,6 @@ class ConsumerPlugin(
|
|||||||
}
|
}
|
||||||
CustomFieldInstance.objects.create(**args) # adds to document
|
CustomFieldInstance.objects.create(**args) # adds to document
|
||||||
|
|
||||||
self._store_barcodes(document)
|
|
||||||
|
|
||||||
def _read_version_barcodes(self, mime_type: str, work_dir: Path) -> None:
|
|
||||||
barcode_settings = BarcodeConfig()
|
|
||||||
if not barcode_settings.barcode_store_values:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.metadata.barcodes = (
|
|
||||||
read_barcode_values(
|
|
||||||
self.working_copy,
|
|
||||||
mime_type,
|
|
||||||
barcode_settings,
|
|
||||||
work_dir,
|
|
||||||
)
|
|
||||||
or None
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.warning(f"Could not read barcodes of {self.filename}: {e}")
|
|
||||||
|
|
||||||
def _store_barcodes(self, document: Document) -> None:
|
|
||||||
if self.metadata.barcodes:
|
|
||||||
DocumentBarcode.objects.bulk_create(
|
|
||||||
DocumentBarcode(document=document, **barcode)
|
|
||||||
for barcode in self.metadata.barcodes
|
|
||||||
)
|
|
||||||
|
|
||||||
def _write(self, source, target) -> None:
|
def _write(self, source, target) -> None:
|
||||||
with (
|
with (
|
||||||
Path(source).open("rb") as read_file,
|
Path(source).open("rb") as read_file,
|
||||||
|
|||||||
@@ -9,16 +9,6 @@ from guardian.shortcuts import get_groups_with_perms
|
|||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
|
|
||||||
|
|
||||||
class StoredBarcode(TypedDict):
|
|
||||||
"""
|
|
||||||
A detected barcode as it is stored with a document
|
|
||||||
"""
|
|
||||||
|
|
||||||
page: int # 1-indexed
|
|
||||||
value: str
|
|
||||||
format: str # a DocumentBarcode.Format value
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class DocumentMetadataOverrides:
|
class DocumentMetadataOverrides:
|
||||||
"""
|
"""
|
||||||
@@ -45,7 +35,6 @@ class DocumentMetadataOverrides:
|
|||||||
version_label: str | None = None
|
version_label: str | None = None
|
||||||
actor_id: int | None = None
|
actor_id: int | None = None
|
||||||
remote_ocr: bool = False
|
remote_ocr: bool = False
|
||||||
barcodes: list[StoredBarcode] | None = None
|
|
||||||
|
|
||||||
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
|
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ from documents.models import Correspondent
|
|||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentBarcode
|
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
from documents.models import SavedView
|
from documents.models import SavedView
|
||||||
@@ -354,7 +353,6 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
"workflows": Workflow.objects.all(),
|
"workflows": Workflow.objects.all(),
|
||||||
"custom_fields": CustomField.objects.all(),
|
"custom_fields": CustomField.objects.all(),
|
||||||
"custom_field_instances": CustomFieldInstance.global_objects.all(),
|
"custom_field_instances": CustomFieldInstance.global_objects.all(),
|
||||||
"document_barcodes": DocumentBarcode.objects.all(),
|
|
||||||
"app_configs": ApplicationConfiguration.objects.all(),
|
"app_configs": ApplicationConfiguration.objects.all(),
|
||||||
"notes": Note.global_objects.all(),
|
"notes": Note.global_objects.all(),
|
||||||
"documents": Document.global_objects.order_by("id").all(),
|
"documents": Document.global_objects.order_by("id").all(),
|
||||||
@@ -413,7 +411,6 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
elif self.split_manifest and key in (
|
elif self.split_manifest and key in (
|
||||||
"notes",
|
"notes",
|
||||||
"custom_field_instances",
|
"custom_field_instances",
|
||||||
"document_barcodes",
|
|
||||||
):
|
):
|
||||||
# Written per-document in _write_split_manifest
|
# Written per-document in _write_split_manifest
|
||||||
pass
|
pass
|
||||||
@@ -654,12 +651,6 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
CustomFieldInstance.global_objects.filter(document=document),
|
CustomFieldInstance.global_objects.filter(document=document),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
content.extend(
|
|
||||||
serializers.serialize(
|
|
||||||
"python",
|
|
||||||
DocumentBarcode.objects.filter(document=document),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
|
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
manifest_name = Path("json") / manifest_name
|
manifest_name = Path("json") / manifest_name
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ class Command(PaperlessCommand):
|
|||||||
"notes__user",
|
"notes__user",
|
||||||
"custom_fields__field",
|
"custom_fields__field",
|
||||||
"versions",
|
"versions",
|
||||||
"barcodes",
|
|
||||||
"versions__barcodes",
|
|
||||||
)
|
)
|
||||||
total = documents.count()
|
total = documents.count()
|
||||||
rebuild_kwargs = {}
|
rebuild_kwargs = {}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# Generated by Django 5.2.16 on 2026-09-30 23:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("documents", "0026_alter_document_archive_checksum_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="DocumentBarcode",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"page",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
help_text="Page of the original file, starting at 1",
|
|
||||||
verbose_name="page",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("value", models.TextField(verbose_name="value")),
|
|
||||||
(
|
|
||||||
"format",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("Codabar", "Codabar"),
|
|
||||||
("Code39", "Code 39"),
|
|
||||||
("Code39Std", "Code 39 Standard"),
|
|
||||||
("Code39Ext", "Code 39 Extended"),
|
|
||||||
("Code32", "Code 32"),
|
|
||||||
("PZN", "Pharmazentralnummer"),
|
|
||||||
("Code93", "Code 93"),
|
|
||||||
("Code128", "Code 128"),
|
|
||||||
("ITF", "ITF"),
|
|
||||||
("ITF14", "ITF-14"),
|
|
||||||
("DataBar", "DataBar"),
|
|
||||||
("DataBarOmni", "DataBar Omni"),
|
|
||||||
("DataBarStk", "DataBar Stacked"),
|
|
||||||
("DataBarStkOmni", "DataBar Stacked Omni"),
|
|
||||||
("DataBarLtd", "DataBar Limited"),
|
|
||||||
("DataBarExp", "DataBar Expanded"),
|
|
||||||
("DataBarExpStk", "DataBar Expanded Stacked"),
|
|
||||||
("EANUPC", "EAN/UPC"),
|
|
||||||
("EAN13", "EAN-13"),
|
|
||||||
("EAN8", "EAN-8"),
|
|
||||||
("EAN5", "EAN-5"),
|
|
||||||
("EAN2", "EAN-2"),
|
|
||||||
("ISBN", "ISBN"),
|
|
||||||
("UPCA", "UPC-A"),
|
|
||||||
("UPCE", "UPC-E"),
|
|
||||||
("Telepen", "Telepen"),
|
|
||||||
("TelepenAlpha", "Telepen Alpha"),
|
|
||||||
("TelepenNumeric", "Telepen Numeric"),
|
|
||||||
("OtherBarcode", "Other barcode"),
|
|
||||||
("DXFilmEdge", "DX Film Edge"),
|
|
||||||
("PDF417", "PDF417"),
|
|
||||||
("CompactPDF417", "Compact PDF417"),
|
|
||||||
("MicroPDF417", "MicroPDF417"),
|
|
||||||
("Aztec", "Aztec"),
|
|
||||||
("AztecCode", "Aztec Code"),
|
|
||||||
("AztecRune", "Aztec Rune"),
|
|
||||||
("QRCode", "QR Code"),
|
|
||||||
("QRCodeModel1", "QR Code Model 1"),
|
|
||||||
("QRCodeModel2", "QR Code Model 2"),
|
|
||||||
("MicroQRCode", "Micro QR Code"),
|
|
||||||
("RMQRCode", "rMQR Code"),
|
|
||||||
("DataMatrix", "Data Matrix"),
|
|
||||||
("MaxiCode", "MaxiCode"),
|
|
||||||
],
|
|
||||||
max_length=32,
|
|
||||||
verbose_name="format",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"document",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="barcodes",
|
|
||||||
to="documents.document",
|
|
||||||
verbose_name="document",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "document barcode",
|
|
||||||
"verbose_name_plural": "document barcodes",
|
|
||||||
"ordering": ("page", "id"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -366,16 +366,6 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
|||||||
res += f" {self.title}"
|
res += f" {self.title}"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_effective_barcodes(self) -> list["DocumentBarcode"]:
|
|
||||||
"""
|
|
||||||
Returns the stored barcodes for the document, like
|
|
||||||
get_effective_content(): for root documents those of the latest
|
|
||||||
version when there is one, as that is the file users see.
|
|
||||||
"""
|
|
||||||
from documents.versioning import latest_version
|
|
||||||
|
|
||||||
return list(latest_version(self).barcodes.all())
|
|
||||||
|
|
||||||
def get_effective_content(self) -> str | None:
|
def get_effective_content(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
Returns the effective content for the document.
|
Returns the effective content for the document.
|
||||||
@@ -979,87 +969,6 @@ class Note(SoftDeleteModel):
|
|||||||
return self.note
|
return self.note
|
||||||
|
|
||||||
|
|
||||||
class DocumentBarcode(models.Model):
|
|
||||||
"""
|
|
||||||
A barcode found in a document during consumption, kept so its content
|
|
||||||
can be shown and copied
|
|
||||||
"""
|
|
||||||
|
|
||||||
document = models.ForeignKey(
|
|
||||||
Document,
|
|
||||||
related_name="barcodes",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
page = models.PositiveIntegerField(
|
|
||||||
_("page"),
|
|
||||||
help_text=_("Page of the original file, starting at 1"),
|
|
||||||
)
|
|
||||||
|
|
||||||
value = models.TextField(_("value"))
|
|
||||||
|
|
||||||
class Format(models.TextChoices):
|
|
||||||
"""
|
|
||||||
The concrete barcode formats of zxing-cpp, keyed on the enum name.
|
|
||||||
The labels are symbology names and aren't translated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
CODABAR = "Codabar", "Codabar"
|
|
||||||
CODE39 = "Code39", "Code 39"
|
|
||||||
CODE39_STD = "Code39Std", "Code 39 Standard"
|
|
||||||
CODE39_EXT = "Code39Ext", "Code 39 Extended"
|
|
||||||
CODE32 = "Code32", "Code 32"
|
|
||||||
PZN = "PZN", "Pharmazentralnummer"
|
|
||||||
CODE93 = "Code93", "Code 93"
|
|
||||||
CODE128 = "Code128", "Code 128"
|
|
||||||
ITF = "ITF", "ITF"
|
|
||||||
ITF14 = "ITF14", "ITF-14"
|
|
||||||
DATA_BAR = "DataBar", "DataBar"
|
|
||||||
DATA_BAR_OMNI = "DataBarOmni", "DataBar Omni"
|
|
||||||
DATA_BAR_STK = "DataBarStk", "DataBar Stacked"
|
|
||||||
DATA_BAR_STK_OMNI = "DataBarStkOmni", "DataBar Stacked Omni"
|
|
||||||
DATA_BAR_LTD = "DataBarLtd", "DataBar Limited"
|
|
||||||
DATA_BAR_EXP = "DataBarExp", "DataBar Expanded"
|
|
||||||
DATA_BAR_EXP_STK = "DataBarExpStk", "DataBar Expanded Stacked"
|
|
||||||
EANUPC = "EANUPC", "EAN/UPC"
|
|
||||||
EAN13 = "EAN13", "EAN-13"
|
|
||||||
EAN8 = "EAN8", "EAN-8"
|
|
||||||
EAN5 = "EAN5", "EAN-5"
|
|
||||||
EAN2 = "EAN2", "EAN-2"
|
|
||||||
ISBN = "ISBN", "ISBN"
|
|
||||||
UPCA = "UPCA", "UPC-A"
|
|
||||||
UPCE = "UPCE", "UPC-E"
|
|
||||||
TELEPEN = "Telepen", "Telepen"
|
|
||||||
TELEPEN_ALPHA = "TelepenAlpha", "Telepen Alpha"
|
|
||||||
TELEPEN_NUMERIC = "TelepenNumeric", "Telepen Numeric"
|
|
||||||
OTHER_BARCODE = "OtherBarcode", "Other barcode"
|
|
||||||
DX_FILM_EDGE = "DXFilmEdge", "DX Film Edge"
|
|
||||||
PDF417 = "PDF417", "PDF417"
|
|
||||||
COMPACT_PDF417 = "CompactPDF417", "Compact PDF417"
|
|
||||||
MICRO_PDF417 = "MicroPDF417", "MicroPDF417"
|
|
||||||
AZTEC = "Aztec", "Aztec"
|
|
||||||
AZTEC_CODE = "AztecCode", "Aztec Code"
|
|
||||||
AZTEC_RUNE = "AztecRune", "Aztec Rune"
|
|
||||||
QR_CODE = "QRCode", "QR Code"
|
|
||||||
QR_CODE_MODEL1 = "QRCodeModel1", "QR Code Model 1"
|
|
||||||
QR_CODE_MODEL2 = "QRCodeModel2", "QR Code Model 2"
|
|
||||||
MICRO_QR_CODE = "MicroQRCode", "Micro QR Code"
|
|
||||||
RMQR_CODE = "RMQRCode", "rMQR Code"
|
|
||||||
DATA_MATRIX = "DataMatrix", "Data Matrix"
|
|
||||||
MAXI_CODE = "MaxiCode", "MaxiCode"
|
|
||||||
|
|
||||||
format = models.CharField(_("format"), max_length=32, choices=Format.choices)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("page", "id")
|
|
||||||
verbose_name = _("document barcode")
|
|
||||||
verbose_name_plural = _("document barcodes")
|
|
||||||
|
|
||||||
def __str__(self) -> str: # pragma: no cover
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
class ShareLink(SoftDeleteModel):
|
class ShareLink(SoftDeleteModel):
|
||||||
class FileVersion(models.TextChoices):
|
class FileVersion(models.TextChoices):
|
||||||
ARCHIVE = ("archive", _("Archive"))
|
ARCHIVE = ("archive", _("Archive"))
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""
|
||||||
|
Pure PDF page operations used by documents.bulk_edit.
|
||||||
|
|
||||||
|
This module deliberately knows nothing about Django, Celery or the documents
|
||||||
|
app: callers resolve documents, choose output paths and queue work. Every
|
||||||
|
function that writes a PDF removes unreferenced resources before saving.
|
||||||
|
|
||||||
|
pikepdf is always called as ``pikepdf.open(...)`` / ``pikepdf.new()`` (never
|
||||||
|
``from pikepdf import open``) so tests can patch those module attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import pikepdf
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
|
||||||
|
class PageSpec(NamedTuple):
|
||||||
|
"""One page of an output PDF: a 1-indexed source page, optionally rotated."""
|
||||||
|
|
||||||
|
page: int
|
||||||
|
rotate: int = 0 # relative degrees, 0 leaves the page alone
|
||||||
|
|
||||||
|
|
||||||
|
def _require_positive(pages: Iterable[int]) -> None:
|
||||||
|
for page in pages:
|
||||||
|
if page < 1:
|
||||||
|
raise ValueError(f"Page numbers start at 1, got {page}")
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_pdf(src: Path, dst: Path, degrees: int) -> None:
|
||||||
|
"""
|
||||||
|
Rotate every page relatively on the opened document, not a rebuild, so Info,
|
||||||
|
XMP and outlines are kept. ``src`` is not modified.
|
||||||
|
"""
|
||||||
|
with pikepdf.open(src) as pdf:
|
||||||
|
for page in pdf.pages:
|
||||||
|
page.rotate(degrees, relative=True)
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
pdf.save(dst)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_pages(src: Path, dst: Path, pages: Iterable[int]) -> None:
|
||||||
|
"""
|
||||||
|
Remove 1-indexed pages from the opened document, not a rebuild, so Info, XMP
|
||||||
|
and outlines are kept. ``src`` is not modified.
|
||||||
|
|
||||||
|
Duplicates are ignored. Pages are removed highest first so earlier removals
|
||||||
|
never shift the index of later ones.
|
||||||
|
"""
|
||||||
|
unique = sorted(set(pages))
|
||||||
|
_require_positive(unique)
|
||||||
|
with pikepdf.open(src) as pdf:
|
||||||
|
for page_num in reversed(unique):
|
||||||
|
del pdf.pages[page_num - 1]
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
pdf.save(dst)
|
||||||
|
|
||||||
|
|
||||||
|
def build_pdfs(
|
||||||
|
src: Path,
|
||||||
|
outputs: Sequence[tuple[Sequence[PageSpec], Callable[[], Path]]],
|
||||||
|
) -> list[Path]:
|
||||||
|
"""
|
||||||
|
Build one new PDF per output from pages of ``src``, opening ``src`` once.
|
||||||
|
|
||||||
|
Each output is ``(page_specs, make_dst)``. ``make_dst`` is called after that
|
||||||
|
output's pages are copied and immediately before it is saved, so a bad page
|
||||||
|
number never leaves a destination behind. Document-level data (Info, XMP,
|
||||||
|
outlines) is not carried over. Returns the written paths in output order.
|
||||||
|
"""
|
||||||
|
for specs, _ in outputs:
|
||||||
|
_require_positive(spec.page for spec in specs)
|
||||||
|
|
||||||
|
written: list[Path] = []
|
||||||
|
with pikepdf.open(src) as source:
|
||||||
|
for specs, make_dst in outputs:
|
||||||
|
dst = pikepdf.new()
|
||||||
|
for spec in specs:
|
||||||
|
dst.pages.append(source.pages[spec.page - 1])
|
||||||
|
if spec.rotate:
|
||||||
|
dst.pages[-1].rotate(spec.rotate, relative=True)
|
||||||
|
dst.remove_unreferenced_resources()
|
||||||
|
path = make_dst()
|
||||||
|
dst.save(path)
|
||||||
|
dst.close()
|
||||||
|
written.append(path)
|
||||||
|
return written
|
||||||
|
|
||||||
|
|
||||||
|
def validate_page_operations(
|
||||||
|
operations: Sequence[Mapping[str, int]],
|
||||||
|
*,
|
||||||
|
single_output: bool,
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Validate ``edit_pdf`` style operations and return the output document count.
|
||||||
|
|
||||||
|
Each operation has ``page`` and optionally ``rotate`` and ``doc`` (the output
|
||||||
|
document index, default 0). The bounds rule is kept as it was: a ``doc`` index
|
||||||
|
must be below the number of operations.
|
||||||
|
"""
|
||||||
|
if not operations:
|
||||||
|
raise ValueError("Output document index is out of bounds")
|
||||||
|
|
||||||
|
max_idx = max(op.get("doc", 0) for op in operations)
|
||||||
|
if single_output and max_idx > 0:
|
||||||
|
raise ValueError("Multiple output documents specified")
|
||||||
|
|
||||||
|
if any(
|
||||||
|
op.get("doc", 0) < 0 or op.get("doc", 0) >= len(operations) for op in operations
|
||||||
|
):
|
||||||
|
raise ValueError("Output document index is out of bounds")
|
||||||
|
|
||||||
|
return max_idx + 1
|
||||||
|
|
||||||
|
|
||||||
|
def needs_decrypt(src: Path) -> bool:
|
||||||
|
"""
|
||||||
|
True if ``src`` is encrypted. A PDF that needs a password to open at all
|
||||||
|
counts as encrypted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with pikepdf.open(src) as pdf:
|
||||||
|
return bool(pdf.is_encrypted)
|
||||||
|
except pikepdf.PasswordError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_pdf(src: Path, make_dst: Callable[[], Path], password: str) -> Path:
|
||||||
|
"""
|
||||||
|
Write an unencrypted copy of ``src`` and return its path.
|
||||||
|
|
||||||
|
``make_dst`` is only called once the password has been accepted, so a wrong
|
||||||
|
password never leaves a destination behind.
|
||||||
|
"""
|
||||||
|
with pikepdf.open(src, password=password) as pdf:
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
dst = make_dst()
|
||||||
|
pdf.save(dst)
|
||||||
|
return dst
|
||||||
|
|
||||||
|
|
||||||
|
class PdfMerger:
|
||||||
|
"""
|
||||||
|
Accumulates the pages of several PDFs into one new PDF.
|
||||||
|
|
||||||
|
``add`` raises if a source cannot be read; deciding whether to skip it is the
|
||||||
|
caller's policy. Use as a context manager so the merged PDF is closed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._pdf = pikepdf.new()
|
||||||
|
self._version: str = self._pdf.pdf_version
|
||||||
|
|
||||||
|
def __enter__(self) -> PdfMerger:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type[BaseException] | None,
|
||||||
|
exc: BaseException | None,
|
||||||
|
tb: TracebackType | None,
|
||||||
|
) -> None:
|
||||||
|
self._pdf.close()
|
||||||
|
|
||||||
|
def add(self, path: Path) -> None:
|
||||||
|
with pikepdf.open(str(path)) as pdf:
|
||||||
|
self._version = max(self._version, pdf.pdf_version)
|
||||||
|
self._pdf.pages.extend(pdf.pages)
|
||||||
|
|
||||||
|
def save(self, dst: Path) -> None:
|
||||||
|
self._pdf.remove_unreferenced_resources()
|
||||||
|
self._pdf.save(dst, min_version=self._version)
|
||||||
@@ -311,13 +311,7 @@ class WriteBatch:
|
|||||||
queryset = annotate_effective_content(
|
queryset = annotate_effective_content(
|
||||||
Document.objects.filter(pk__in=ids)
|
Document.objects.filter(pk__in=ids)
|
||||||
.select_related("correspondent", "document_type", "storage_path", "owner")
|
.select_related("correspondent", "document_type", "storage_path", "owner")
|
||||||
.prefetch_related(
|
.prefetch_related("tags", "notes__user", "custom_fields__field"),
|
||||||
"tags",
|
|
||||||
"notes__user",
|
|
||||||
"custom_fields__field",
|
|
||||||
"barcodes",
|
|
||||||
"versions__barcodes",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
for document, grant in _DocumentViewerStream(queryset, chunk_size=1000):
|
for document, grant in _DocumentViewerStream(queryset, chunk_size=1000):
|
||||||
self.remove(document.pk)
|
self.remove(document.pk)
|
||||||
@@ -610,16 +604,6 @@ class TantivyBackend:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Barcodes: JSON field like custom_fields, only filled when stored
|
|
||||||
for barcode in document.get_effective_barcodes():
|
|
||||||
doc.add_json(
|
|
||||||
"barcodes",
|
|
||||||
{
|
|
||||||
"value": normalize_search_text(barcode.value),
|
|
||||||
"format": normalize_search_text(barcode.format),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dates
|
# Dates
|
||||||
created_date = datetime(
|
created_date = datetime(
|
||||||
document.created.year,
|
document.created.year,
|
||||||
|
|||||||
@@ -39,9 +39,4 @@ PUBLIC_FIELDS: tuple[FieldSpec, ...] = (
|
|||||||
FieldKind.JSON,
|
FieldKind.JSON,
|
||||||
subpaths={"name": SubpathSpec(), "value": SubpathSpec(default=True)},
|
subpaths={"name": SubpathSpec(), "value": SubpathSpec(default=True)},
|
||||||
),
|
),
|
||||||
FieldSpec(
|
|
||||||
"barcodes",
|
|
||||||
FieldKind.JSON,
|
|
||||||
subpaths={"value": SubpathSpec(default=True), "format": SubpathSpec()},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
@@ -25,8 +25,7 @@ logger = logging.getLogger("paperless.search")
|
|||||||
# order, and the write-only correspondent/document_type/storage_path/tag id
|
# order, and the write-only correspondent/document_type/storage_path/tag id
|
||||||
# columns dropped. tantivy compares schemas by ordered field list, so an
|
# columns dropped. tantivy compares schemas by ordered field list, so an
|
||||||
# index built by v1 rejects every write against the v2 schema.
|
# index built by v1 rejects every write against the v2 schema.
|
||||||
# v3 - barcodes JSON field for stored barcode contents
|
SCHEMA_VERSION: Final[int] = 2
|
||||||
SCHEMA_VERSION: Final[int] = 3
|
|
||||||
|
|
||||||
|
|
||||||
class FieldDescriptor(NamedTuple):
|
class FieldDescriptor(NamedTuple):
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ from documents.models import Correspondent
|
|||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentBarcode
|
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
@@ -988,12 +987,6 @@ class BasicUserSerializer(serializers.ModelSerializer[User]):
|
|||||||
fields = ["id", "username", "first_name", "last_name"]
|
fields = ["id", "username", "first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
class DocumentBarcodeSerializer(serializers.ModelSerializer[DocumentBarcode]):
|
|
||||||
class Meta:
|
|
||||||
model = DocumentBarcode
|
|
||||||
fields = ["page", "value", "format"]
|
|
||||||
|
|
||||||
|
|
||||||
class NotesSerializer(serializers.ModelSerializer[Note]):
|
class NotesSerializer(serializers.ModelSerializer[Note]):
|
||||||
user = BasicUserSerializer(read_only=True)
|
user = BasicUserSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -2144,6 +2137,8 @@ class BulkEditSerializer(
|
|||||||
raise serializers.ValidationError("pages must be a list")
|
raise serializers.ValidationError("pages must be a list")
|
||||||
if not all(isinstance(i, int) for i in parameters["pages"]):
|
if not all(isinstance(i, int) for i in parameters["pages"]):
|
||||||
raise serializers.ValidationError("pages must be a list of integers")
|
raise serializers.ValidationError("pages must be a list of integers")
|
||||||
|
if any(i < 1 for i in parameters["pages"]):
|
||||||
|
raise serializers.ValidationError("pages must be positive integers")
|
||||||
|
|
||||||
def _validate_parameters_merge(self, parameters) -> None:
|
def _validate_parameters_merge(self, parameters) -> None:
|
||||||
if "delete_originals" in parameters:
|
if "delete_originals" in parameters:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from filelock import FileLock
|
|||||||
|
|
||||||
from documents import sanity_checker
|
from documents import sanity_checker
|
||||||
from documents.barcodes import BarcodePlugin
|
from documents.barcodes import BarcodePlugin
|
||||||
from documents.barcodes import read_barcode_values
|
|
||||||
from documents.bulk_download import ArchiveOnlyStrategy
|
from documents.bulk_download import ArchiveOnlyStrategy
|
||||||
from documents.bulk_download import OriginalsOnlyStrategy
|
from documents.bulk_download import OriginalsOnlyStrategy
|
||||||
from documents.caching import clear_document_caches
|
from documents.caching import clear_document_caches
|
||||||
@@ -37,7 +36,6 @@ from documents.data_models import ConsumeFileDuplicateResult
|
|||||||
from documents.data_models import ConsumeFileStoppedResult
|
from documents.data_models import ConsumeFileStoppedResult
|
||||||
from documents.data_models import ConsumeFileSuccessResult
|
from documents.data_models import ConsumeFileSuccessResult
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import StoredBarcode
|
|
||||||
from documents.double_sided import CollatePlugin
|
from documents.double_sided import CollatePlugin
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
@@ -45,7 +43,6 @@ from documents.matching import prefilter_documents_by_workflowtrigger
|
|||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentBarcode
|
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.models import ShareLink
|
from documents.models import ShareLink
|
||||||
@@ -70,7 +67,6 @@ from documents.utils import identity
|
|||||||
from documents.versioning import annotate_effective_content
|
from documents.versioning import annotate_effective_content
|
||||||
from documents.workflows.utils import get_workflows_for_trigger
|
from documents.workflows.utils import get_workflows_for_trigger
|
||||||
from paperless.config import AIConfig
|
from paperless.config import AIConfig
|
||||||
from paperless.config import BarcodeConfig
|
|
||||||
from paperless.config import RemoteOCRConfig
|
from paperless.config import RemoteOCRConfig
|
||||||
from paperless.logging import consume_task_id
|
from paperless.logging import consume_task_id
|
||||||
from paperless.parsers import ParserContext
|
from paperless.parsers import ParserContext
|
||||||
@@ -345,29 +341,6 @@ def bulk_update_documents(document_ids) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _read_barcodes_for_reprocess(document: Document) -> list[StoredBarcode] | None:
|
|
||||||
"""
|
|
||||||
Reads the barcodes of the original again, e.g. for documents consumed
|
|
||||||
before storing them was enabled. Returns None if they should be left as
|
|
||||||
they are: storing is off, the file can't be scanned with the current
|
|
||||||
settings, or the scan failed.
|
|
||||||
"""
|
|
||||||
barcode_settings = BarcodeConfig()
|
|
||||||
if not barcode_settings.barcode_store_values:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
with TemporaryDirectory(dir=settings.SCRATCH_DIR) as tmpdir:
|
|
||||||
return read_barcode_values(
|
|
||||||
document.source_path,
|
|
||||||
document.mime_type,
|
|
||||||
barcode_settings,
|
|
||||||
Path(tmpdir),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Could not read barcodes of document {document}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def update_document_content_maybe_archive_file(
|
def update_document_content_maybe_archive_file(
|
||||||
document_id,
|
document_id,
|
||||||
@@ -414,8 +387,6 @@ def update_document_content_maybe_archive_file(
|
|||||||
produce_archive=produce_archive,
|
produce_archive=produce_archive,
|
||||||
)
|
)
|
||||||
|
|
||||||
barcodes = _read_barcodes_for_reprocess(document)
|
|
||||||
|
|
||||||
thumbnail = parser.get_thumbnail(document.source_path, mime_type)
|
thumbnail = parser.get_thumbnail(document.source_path, mime_type)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -472,17 +443,6 @@ def update_document_content_maybe_archive_file(
|
|||||||
action=LogEntry.Action.UPDATE,
|
action=LogEntry.Action.UPDATE,
|
||||||
)
|
)
|
||||||
|
|
||||||
if barcodes is not None:
|
|
||||||
document.barcodes.all().delete()
|
|
||||||
DocumentBarcode.objects.bulk_create(
|
|
||||||
DocumentBarcode(document=document, **barcode)
|
|
||||||
for barcode in barcodes
|
|
||||||
)
|
|
||||||
# metadata_etag includes modified
|
|
||||||
Document.objects.filter(pk=document.pk).update(
|
|
||||||
modified=timezone.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
with FileLock(settings.MEDIA_LOCK):
|
with FileLock(settings.MEDIA_LOCK):
|
||||||
if parser.get_archive_path():
|
if parser.get_archive_path():
|
||||||
create_source_path_directory(document.archive_path)
|
create_source_path_directory(document.archive_path)
|
||||||
|
|||||||
Binary file not shown.
@@ -1,95 +0,0 @@
|
|||||||
"""Stored barcode contents in the search index.
|
|
||||||
|
|
||||||
Barcodes are a JSON field like notes and custom fields: barcodes: resolves to
|
|
||||||
barcodes.value:, and a plain query without the prefix does not look at them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from documents.models import DocumentBarcode
|
|
||||||
from paperless_testing.factories import DocumentBarcodeFactory
|
|
||||||
from paperless_testing.factories import DocumentFactory
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
from documents.models import Document
|
|
||||||
from documents.search._backend import TantivyBackend
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.search, pytest.mark.django_db]
|
|
||||||
|
|
||||||
|
|
||||||
class TestBarcodeSearch:
|
|
||||||
@pytest.fixture
|
|
||||||
def with_barcodes(self, backend: TantivyBackend) -> Document:
|
|
||||||
document = DocumentFactory(title="Letter", content="x")
|
|
||||||
DocumentBarcodeFactory(
|
|
||||||
document=document,
|
|
||||||
value="WIFI:T:WPA;S:Guest-WLAN;P:crocodile123;;",
|
|
||||||
)
|
|
||||||
DocumentBarcodeFactory(
|
|
||||||
document=document,
|
|
||||||
page=2,
|
|
||||||
value="DE89370400440532013000",
|
|
||||||
format=DocumentBarcode.Format.CODE128,
|
|
||||||
)
|
|
||||||
backend.add_or_update(document)
|
|
||||||
return document
|
|
||||||
|
|
||||||
def test_bare_barcodes_prefix_searches_values(
|
|
||||||
self,
|
|
||||||
with_barcodes: Document,
|
|
||||||
index_document: Callable[..., Document],
|
|
||||||
matched_ids: Callable[[str], set[int]],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- A document with stored barcodes, and a decoy document whose
|
|
||||||
content (not a barcode) contains the same word
|
|
||||||
WHEN:
|
|
||||||
- A bare "barcodes:" prefix query is run
|
|
||||||
THEN:
|
|
||||||
- Only the document whose barcode matches is returned, also for a
|
|
||||||
part of a barcode between separators
|
|
||||||
"""
|
|
||||||
index_document(title="Decoy", content="crocodile123 in the text")
|
|
||||||
|
|
||||||
assert matched_ids("barcodes:crocodile123") == {with_barcodes.pk}
|
|
||||||
assert matched_ids("barcodes.value:DE89370400440532013000") == {
|
|
||||||
with_barcodes.pk,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_barcodes_format_subpath(
|
|
||||||
self,
|
|
||||||
with_barcodes: Document,
|
|
||||||
matched_ids: Callable[[str], set[int]],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- A document with a QR code and a Code 128 barcode
|
|
||||||
WHEN:
|
|
||||||
- The format subpath is queried
|
|
||||||
THEN:
|
|
||||||
- The document is found by its barcode formats
|
|
||||||
"""
|
|
||||||
assert matched_ids("barcodes.format:qrcode") == {with_barcodes.pk}
|
|
||||||
assert matched_ids("barcodes.format:aztec") == set()
|
|
||||||
|
|
||||||
def test_plain_query_ignores_barcodes(
|
|
||||||
self,
|
|
||||||
with_barcodes: Document,
|
|
||||||
matched_ids: Callable[[str], set[int]],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- A document with a barcode value not found in its text
|
|
||||||
WHEN:
|
|
||||||
- The value is searched without a field prefix
|
|
||||||
THEN:
|
|
||||||
- Nothing is found, as with notes and custom fields
|
|
||||||
"""
|
|
||||||
assert matched_ids("crocodile123") == set()
|
|
||||||
@@ -8,7 +8,7 @@ queryable-but-always-empty -- syntactically valid, silently matching
|
|||||||
nothing -- with no test failure anywhere.
|
nothing -- with no test failure anywhere.
|
||||||
|
|
||||||
This indexes one real document carrying values for every JSON field
|
This indexes one real document carrying values for every JSON field
|
||||||
(a Note, a CustomFieldInstance, a DocumentBarcode) and inspects the document's own stored
|
(a Note, a CustomFieldInstance) and inspects the document's own stored
|
||||||
JSON payload, rather than running field-specific queries: that way a
|
JSON payload, rather than running field-specific queries: that way a
|
||||||
future JSON field's subpaths are covered automatically, without a new
|
future JSON field's subpaths are covered automatically, without a new
|
||||||
per-subpath query having to be added by hand each time.
|
per-subpath query having to be added by hand each time.
|
||||||
@@ -27,7 +27,6 @@ from documents.models import CustomFieldInstance
|
|||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
from documents.search._fields import PUBLIC_FIELDS
|
from documents.search._fields import PUBLIC_FIELDS
|
||||||
from paperless_testing.factories import DocumentBarcodeFactory
|
|
||||||
from paperless_testing.factories import UserFactory
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -43,12 +42,11 @@ class TestJsonSubpathsAreWrittenAtIndexTime:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- A document with a Note, a CustomFieldInstance and a
|
- A document with a Note and a CustomFieldInstance attached
|
||||||
DocumentBarcode attached
|
|
||||||
WHEN:
|
WHEN:
|
||||||
- The document is indexed via TantivyBackend.add_or_update
|
- The document is indexed via TantivyBackend.add_or_update
|
||||||
THEN:
|
THEN:
|
||||||
- Every subpath PUBLIC_FIELDS declares for notes/custom_fields/barcodes
|
- Every subpath PUBLIC_FIELDS declares for notes/custom_fields
|
||||||
is present as a key in the document's stored JSON payload
|
is present as a key in the document's stored JSON payload
|
||||||
"""
|
"""
|
||||||
user = UserFactory(username="completeness-user")
|
user = UserFactory(username="completeness-user")
|
||||||
@@ -67,7 +65,6 @@ class TestJsonSubpathsAreWrittenAtIndexTime:
|
|||||||
field=field,
|
field=field,
|
||||||
value_text="a value",
|
value_text="a value",
|
||||||
)
|
)
|
||||||
DocumentBarcodeFactory(document=doc, value="a barcode")
|
|
||||||
backend.add_or_update(doc)
|
backend.add_or_update(doc)
|
||||||
|
|
||||||
index = backend._index
|
index = backend._index
|
||||||
|
|||||||
@@ -175,14 +175,6 @@ PINNED_DESCRIPTORS: tuple[FieldDescriptor, ...] = (
|
|||||||
fast=False,
|
fast=False,
|
||||||
tokenizer="paperless_text",
|
tokenizer="paperless_text",
|
||||||
),
|
),
|
||||||
FieldDescriptor(
|
|
||||||
"barcodes",
|
|
||||||
"json",
|
|
||||||
stored=True,
|
|
||||||
indexed=True,
|
|
||||||
fast=False,
|
|
||||||
tokenizer="paperless_text",
|
|
||||||
),
|
|
||||||
FieldDescriptor(
|
FieldDescriptor(
|
||||||
"title_sort",
|
"title_sort",
|
||||||
"text",
|
"text",
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
"barcode_enable_tag": None,
|
"barcode_enable_tag": None,
|
||||||
"barcode_tag_mapping": None,
|
"barcode_tag_mapping": None,
|
||||||
"barcode_tag_split": None,
|
"barcode_tag_split": None,
|
||||||
"barcode_store_values": None,
|
|
||||||
"remote_ocr_engine": None,
|
"remote_ocr_engine": None,
|
||||||
"remote_ocr_api_key": None,
|
"remote_ocr_api_key": None,
|
||||||
"remote_ocr_endpoint": None,
|
"remote_ocr_endpoint": None,
|
||||||
|
|||||||
@@ -1843,6 +1843,36 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
m.assert_called_once()
|
m.assert_called_once()
|
||||||
self.assertEqual(m.call_args.kwargs["pages"], [[1], [2, 3, 4], [5]])
|
self.assertEqual(m.call_args.kwargs["pages"], [[1], [2, 3, 4], [5]])
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.delete_pages")
|
||||||
|
def test_bulk_edit_delete_pages_rejects_pages_below_one(self, m) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A legacy delete_pages bulk edit
|
||||||
|
WHEN:
|
||||||
|
- API to bulk edit is called with a page number below 1
|
||||||
|
THEN:
|
||||||
|
- API returns HTTP 400
|
||||||
|
- delete_pages is not called
|
||||||
|
"""
|
||||||
|
self.setup_mock(m, "delete_pages")
|
||||||
|
|
||||||
|
for pages in ([0], [-1], [1, 0]):
|
||||||
|
with self.subTest(pages=pages):
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "delete_pages",
|
||||||
|
"parameters": {"pages": pages},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"pages must be positive integers", response.content)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
@mock.patch("documents.views.bulk_edit.rotate")
|
@mock.patch("documents.views.bulk_edit.rotate")
|
||||||
def test_rotate_insufficient_permissions(self, m) -> None:
|
def test_rotate_insufficient_permissions(self, m) -> None:
|
||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
|
|||||||
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user