Merge branch 'dev' into feature-ai

This commit is contained in:
shamoon
2025-09-04 09:16:56 -07:00
committed by GitHub
29 changed files with 2490 additions and 1804 deletions

View File

@@ -3,7 +3,7 @@
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml", "dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
"service": "paperless-development", "service": "paperless-development",
"workspaceFolder": "/usr/src/paperless/paperless-ngx", "workspaceFolder": "/usr/src/paperless/paperless-ngx",
"postCreateCommand": "/bin/bash -c 'uv sync --group dev && uv run pre-commit install'", "postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'",
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install python - name: Install python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@@ -40,7 +40,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -90,7 +90,7 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Start containers - name: Start containers
run: | run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
@@ -162,7 +162,7 @@ jobs:
needs: needs:
- pre-commit - pre-commit
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
@@ -195,7 +195,7 @@ jobs:
shard-index: [1, 2, 3, 4] shard-index: [1, 2, 3, 4]
shard-count: [4] shard-count: [4]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
@@ -245,7 +245,7 @@ jobs:
shard-index: [1, 2] shard-index: [1, 2]
shard-count: [2] shard-count: [2]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
@@ -288,7 +288,7 @@ jobs:
- tests-frontend - tests-frontend
- tests-frontend-e2e - tests-frontend-e2e
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
@@ -363,7 +363,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
# If https://github.com/docker/buildx/issues/1044 is resolved, # If https://github.com/docker/buildx/issues/1044 is resolved,
# the append input with a native arm64 arch could be used to # the append input with a native arm64 arch could be used to
# significantly speed up building # significantly speed up building
@@ -433,7 +433,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -453,12 +453,12 @@ jobs:
sudo apt-get update -qq sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext liblept5 sudo apt-get install -qq --no-install-recommends gettext liblept5
- name: Download frontend artifact - name: Download frontend artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
name: frontend-compiled name: frontend-compiled
path: src/documents/static/frontend/ path: src/documents/static/frontend/
- name: Download documentation artifact - name: Download documentation artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
name: documentation name: documentation
path: docs/_build/html/ path: docs/_build/html/
@@ -538,7 +538,7 @@ jobs:
if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc')) if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc'))
steps: steps:
- name: Download release artifact - name: Download release artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
name: release name: release
path: ./ path: ./
@@ -579,7 +579,7 @@ jobs:
if: needs.publish-release.outputs.prerelease == 'false' if: needs.publish-release.outputs.prerelease == 'false'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
ref: main ref: main
- name: Set up Python - name: Set up Python

View File

@@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.10.0 uses: stumpylog/image-cleaner-action/ephemeral@v0.11.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
@@ -54,7 +54,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.10.0 uses: stumpylog/image-cleaner-action/untagged@v0.11.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"

View File

@@ -34,7 +34,7 @@ jobs:
# Learn more about CodeQL language support at https://git.io/codeql-language-support # Learn more about CodeQL language support at https://git.io/codeql-language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v3

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
token: ${{ secrets.PNGX_BOT_PAT }} token: ${{ secrets.PNGX_BOT_PAT }}
- name: crowdin action - name: crowdin action

View File

@@ -37,7 +37,7 @@ jobs:
labels.push('bug'); labels.push('bug');
} else if (/^feature/i.test(title)) { } else if (/^feature/i.test(title)) {
labels.push('enhancement'); labels.push('enhancement');
} else if (!/^(dependabot)/i.test(title)) { } else if (!/^(dependabot)/i.test(title) && !/^(chore)/i.test(title)) {
labels.push('enhancement'); // Default fallback labels.push('enhancement'); // Default fallback
} }

View File

@@ -11,7 +11,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
token: ${{ secrets.PNGX_BOT_PAT }} token: ${{ secrets.PNGX_BOT_PAT }}
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}

View File

@@ -18,7 +18,7 @@ repos:
exclude_types: exclude_types:
- svg - svg
- pofile - pofile
exclude: "(^LICENSE$)" exclude: "(^LICENSE$|^src/documents/static/bootstrap.min.css$)"
- id: mixed-line-ending - id: mixed-line-ending
args: args:
- "--fix=lf" - "--fix=lf"
@@ -51,7 +51,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2 rev: v0.12.2
hooks: hooks:
- id: ruff - id: ruff-check
- id: ruff-format - id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.6.0" rev: "v2.6.0"

1
.yamlfmt Normal file
View File

@@ -0,0 +1 @@
line_ending: lf

View File

@@ -214,18 +214,9 @@ lint.per-file-ignores."docker/wait-for-redis.py" = [
"INP001", "INP001",
"T201", "T201",
] ]
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/models.py" = [ lint.per-file-ignores."src/documents/models.py" = [
"SIM115", "SIM115",
] ]
lint.per-file-ignores."src/documents/parsers.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [ lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
"RUF001", "RUF001",
] ]

View File

@@ -5,14 +5,14 @@
<trans-unit id="ngb.alert.close" datatype="html"> <trans-unit id="ngb.alert.close" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/alert/alert.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.slide-number" datatype="html"> <trans-unit id="ngb.carousel.slide-number" datatype="html">
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source> <source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">131,135</context> <context context-type="linenumber">131,135</context>
</context-group> </context-group>
<note priority="1" from="description">Currently selected slide number read by screen reader</note> <note priority="1" from="description">Currently selected slide number read by screen reader</note>
@@ -20,212 +20,212 @@
<trans-unit id="ngb.carousel.previous" datatype="html"> <trans-unit id="ngb.carousel.previous" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">157,159</context> <context context-type="linenumber">157,159</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.next" datatype="html"> <trans-unit id="ngb.carousel.next" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">198</context> <context context-type="linenumber">198</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.previous-month" datatype="html"> <trans-unit id="ngb.datepicker.previous-month" datatype="html">
<source>Previous month</source> <source>Previous month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">83,85</context> <context context-type="linenumber">83,85</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.next-month" datatype="html"> <trans-unit id="ngb.datepicker.next-month" datatype="html">
<source>Next month</source> <source>Next month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html"> <trans-unit id="ngb.timepicker.HH" datatype="html">
<source>HH</source> <source>HH</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html"> <trans-unit id="ngb.toast.close-aria" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-month" datatype="html"> <trans-unit id="ngb.datepicker.select-month" datatype="html">
<source>Select month</source> <source>Select month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first" datatype="html"> <trans-unit id="ngb.pagination.first" datatype="html">
<source>««</source> <source>««</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.hours" datatype="html"> <trans-unit id="ngb.timepicker.hours" datatype="html">
<source>Hours</source> <source>Hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous" datatype="html"> <trans-unit id="ngb.pagination.previous" datatype="html">
<source>«</source> <source>«</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html"> <trans-unit id="ngb.timepicker.MM" datatype="html">
<source>MM</source> <source>MM</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html"> <trans-unit id="ngb.pagination.next" datatype="html">
<source>»</source> <source>»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-year" datatype="html"> <trans-unit id="ngb.datepicker.select-year" datatype="html">
<source>Select year</source> <source>Select year</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.minutes" datatype="html"> <trans-unit id="ngb.timepicker.minutes" datatype="html">
<source>Minutes</source> <source>Minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last" datatype="html"> <trans-unit id="ngb.pagination.last" datatype="html">
<source>»»</source> <source>»»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first-aria" datatype="html"> <trans-unit id="ngb.pagination.first-aria" datatype="html">
<source>First</source> <source>First</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-hours" datatype="html"> <trans-unit id="ngb.timepicker.increment-hours" datatype="html">
<source>Increment hours</source> <source>Increment hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous-aria" datatype="html"> <trans-unit id="ngb.pagination.previous-aria" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html"> <trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
<source>Decrement hours</source> <source>Decrement hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next-aria" datatype="html"> <trans-unit id="ngb.pagination.next-aria" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html"> <trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
<source>Increment minutes</source> <source>Increment minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last-aria" datatype="html"> <trans-unit id="ngb.pagination.last-aria" datatype="html">
<source>Last</source> <source>Last</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html"> <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
<source>Decrement minutes</source> <source>Decrement minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.SS" datatype="html"> <trans-unit id="ngb.timepicker.SS" datatype="html">
<source>SS</source> <source>SS</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.seconds" datatype="html"> <trans-unit id="ngb.timepicker.seconds" datatype="html">
<source>Seconds</source> <source>Seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html"> <trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
<source>Increment seconds</source> <source>Increment seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html"> <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
<source>Decrement seconds</source> <source>Decrement seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.PM" datatype="html"> <trans-unit id="ngb.timepicker.PM" datatype="html">
<source><x id="INTERPOLATION"/></source> <source><x id="INTERPOLATION"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -233,7 +233,7 @@
<source><x id="INTERPOLATION" equiv-text="barConfig); <source><x id="INTERPOLATION" equiv-text="barConfig);
pu"/></source> pu"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.1.4_@angular+core@20.1.4_@angular+_4264661dcfc97b5bf5cf26958990f623/node_modules/src/progressbar/progressbar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="linenumber">41,42</context> <context context-type="linenumber">41,42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -553,7 +553,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">55</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@@ -1444,7 +1444,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@@ -3673,42 +3673,42 @@
<source>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</source> <source>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">32</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2739003406164860877" datatype="html"> <trans-unit id="2739003406164860877" datatype="html">
<source>Default Currency</source> <source>Default Currency</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">47</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7615210738790237590" datatype="html"> <trans-unit id="7615210738790237590" datatype="html">
<source>3-character currency code</source> <source>3-character currency code</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">47</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="607636736207886379" datatype="html"> <trans-unit id="607636736207886379" datatype="html">
<source>Use locale</source> <source>Use locale</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">47</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="528950215505228201" datatype="html"> <trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source> <source>Create new custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context>
<context context-type="linenumber">93</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8751213029607178010" datatype="html"> <trans-unit id="8751213029607178010" datatype="html">
<source>Edit custom field</source> <source>Edit custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6672809941092516947" datatype="html"> <trans-unit id="6672809941092516947" datatype="html">

View File

@@ -11,27 +11,27 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^20.1.4", "@angular/cdk": "^20.2.2",
"@angular/common": "~20.1.4", "@angular/common": "~20.2.4",
"@angular/compiler": "~20.1.4", "@angular/compiler": "~20.2.4",
"@angular/core": "~20.1.4", "@angular/core": "~20.2.4",
"@angular/forms": "~20.1.4", "@angular/forms": "~20.2.4",
"@angular/localize": "~20.1.4", "@angular/localize": "~20.2.4",
"@angular/platform-browser": "~20.1.4", "@angular/platform-browser": "~20.2.4",
"@angular/platform-browser-dynamic": "~20.1.4", "@angular/platform-browser-dynamic": "~20.2.4",
"@angular/router": "~20.1.4", "@angular/router": "~20.2.4",
"@ng-bootstrap/ng-bootstrap": "^19.0.1", "@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@ng-select/ng-select": "^20.0.1", "@ng-select/ng-select": "^20.1.3",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.8",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.4.0", "ng2-pdf-viewer": "^10.4.0",
"ngx-bootstrap-icons": "^1.9.3", "ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.0.0", "ngx-color": "^10.0.0",
"ngx-cookie-service": "^20.0.1", "ngx-cookie-service": "^20.1.0",
"ngx-device-detector": "^10.0.2", "ngx-device-detector": "^10.1.0",
"ngx-ui-tour-ng-bootstrap": "^17.0.1", "ngx-ui-tour-ng-bootstrap": "^17.0.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
@@ -42,33 +42,33 @@
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "^20.0.0", "@angular-builders/custom-webpack": "^20.0.0",
"@angular-builders/jest": "^20.0.0", "@angular-builders/jest": "^20.0.0",
"@angular-devkit/core": "^20.1.4", "@angular-devkit/core": "^20.2.2",
"@angular-devkit/schematics": "^20.1.4", "@angular-devkit/schematics": "^20.2.2",
"@angular-eslint/builder": "20.1.1", "@angular-eslint/builder": "20.2.0",
"@angular-eslint/eslint-plugin": "20.1.1", "@angular-eslint/eslint-plugin": "20.2.0",
"@angular-eslint/eslint-plugin-template": "20.1.1", "@angular-eslint/eslint-plugin-template": "20.2.0",
"@angular-eslint/schematics": "20.1.1", "@angular-eslint/schematics": "20.2.0",
"@angular-eslint/template-parser": "20.1.1", "@angular-eslint/template-parser": "20.2.0",
"@angular/build": "^20.1.4", "@angular/build": "^20.2.2",
"@angular/cli": "~20.1.4", "@angular/cli": "~20.2.2",
"@angular/compiler-cli": "~20.1.4", "@angular/compiler-cli": "~20.2.4",
"@codecov/webpack-plugin": "^1.9.1", "@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.54.2", "@playwright/test": "^1.55.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.1.0", "@types/node": "^24.3.0",
"@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/eslint-plugin": "^8.41.0",
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.41.0",
"@typescript-eslint/utils": "^8.38.0", "@typescript-eslint/utils": "^8.41.0",
"eslint": "^9.32.0", "eslint": "^9.34.0",
"jest": "30.0.5", "jest": "30.1.3",
"jest-environment-jsdom": "^30.0.5", "jest-environment-jsdom": "^30.1.2",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"jest-preset-angular": "^15.0.0", "jest-preset-angular": "^15.0.0",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.2.0", "prettier-plugin-organize-imports": "^4.2.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"webpack": "^5.101.0" "webpack": "^5.101.3"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [

3635
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,16 @@
</div> </div>
} }
</div> </div>
@if (allSelectOptions.length > SELECT_OPTION_PAGE_SIZE) {
<ngb-pagination
class="d-flex justify-content-end"
[pageSize]="SELECT_OPTION_PAGE_SIZE"
[collectionSize]="allSelectOptions.length"
[(page)]="selectOptionsPage"
[maxSize]="5"
size="sm"
></ngb-pagination>
}
@if (object?.id) { @if (object?.id) {
<small class="d-block mt-2" i18n>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</small> <small class="d-block mt-2" i18n>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</small>
} }

View File

@@ -125,4 +125,42 @@ describe('CustomFieldEditDialogComponent', () => {
fixture.detectChanges() fixture.detectChanges()
expect(document.activeElement).toBe(selectOptionInputs.last.nativeElement) expect(document.activeElement).toBe(selectOptionInputs.last.nativeElement)
}) })
it('should send all select options including those changed in form on save', () => {
component.dialogMode = EditDialogMode.EDIT
component.object = {
id: 1,
name: 'Field 1',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: Array.from({ length: 50 }, (_, i) => ({
label: `Option ${i + 1}`,
id: `${i + 1}-xyz`,
})),
},
}
fixture.detectChanges()
component.ngOnInit()
component.selectOptionsPage = 2
fixture.detectChanges()
component.objectForm
.get('extra_data')
.get('select_options')
.get('0')
.get('label')
.setValue('Updated Option 9')
const formValues = (component as any).getFormValues()
// first item unchanged
expect(formValues.extra_data.select_options[0]).toEqual({
label: 'Option 1',
id: '1-xyz',
})
// page 2 first item updated
expect(
formValues.extra_data.select_options[component.SELECT_OPTION_PAGE_SIZE]
).toEqual({
label: 'Updated Option 9',
id: '9-xyz',
})
})
}) })

View File

@@ -14,6 +14,7 @@ import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { import {
@@ -28,6 +29,8 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component' import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
const SELECT_OPTION_PAGE_SIZE = 8
@Component({ @Component({
selector: 'pngx-custom-field-edit-dialog', selector: 'pngx-custom-field-edit-dialog',
templateUrl: './custom-field-edit-dialog.component.html', templateUrl: './custom-field-edit-dialog.component.html',
@@ -37,6 +40,7 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
TextComponent, TextComponent,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgbPaginationModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
], ],
}) })
@@ -45,6 +49,21 @@ export class CustomFieldEditDialogComponent
implements OnInit, AfterViewInit implements OnInit, AfterViewInit
{ {
CustomFieldDataType = CustomFieldDataType CustomFieldDataType = CustomFieldDataType
SELECT_OPTION_PAGE_SIZE = SELECT_OPTION_PAGE_SIZE
private _allSelectOptions: any[] = []
public get allSelectOptions(): any[] {
return this._allSelectOptions
}
private _selectOptionsPage: number
public get selectOptionsPage(): number {
return this._selectOptionsPage
}
public set selectOptionsPage(v: number) {
this._selectOptionsPage = v
this.updateSelectOptions()
}
@ViewChildren('selectOption') @ViewChildren('selectOption')
private selectOptionInputs: QueryList<ElementRef> private selectOptionInputs: QueryList<ElementRef>
@@ -67,17 +86,10 @@ export class CustomFieldEditDialogComponent
this.objectForm.get('data_type').disable() this.objectForm.get('data_type').disable()
} }
if (this.object?.data_type === CustomFieldDataType.Select) { if (this.object?.data_type === CustomFieldDataType.Select) {
this.selectOptions.clear() this._allSelectOptions = [
this.object.extra_data.select_options ...(this.object.extra_data.select_options ?? []),
.filter((option) => option) ]
.forEach((option) => this.selectOptionsPage = 1
this.selectOptions.push(
new FormGroup({
label: new FormControl(option.label),
id: new FormControl(option.id),
})
)
)
} }
} }
@@ -87,6 +99,19 @@ export class CustomFieldEditDialogComponent
.subscribe(() => { .subscribe(() => {
this.selectOptionInputs.last?.nativeElement.focus() this.selectOptionInputs.last?.nativeElement.focus()
}) })
this.objectForm.valueChanges
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((change) => {
// Update the relevant select options values if changed in the form, which is only a page of the entire list
this.objectForm
.get('extra_data.select_options')
?.value.forEach((option, index) => {
this._allSelectOptions[
index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE
] = option
})
})
} }
getCreateTitle() { getCreateTitle() {
@@ -108,6 +133,17 @@ export class CustomFieldEditDialogComponent
}) })
} }
protected getFormValues() {
const formValues = super.getFormValues()
if (
this.objectForm.get('data_type')?.value === CustomFieldDataType.Select
) {
// Make sure we send all select options, with updated values
formValues.extra_data.select_options = this._allSelectOptions
}
return formValues
}
getDataTypes() { getDataTypes() {
return DATA_TYPE_LABELS return DATA_TYPE_LABELS
} }
@@ -116,13 +152,35 @@ export class CustomFieldEditDialogComponent
return this.dialogMode === EditDialogMode.EDIT return this.dialogMode === EditDialogMode.EDIT
} }
private updateSelectOptions() {
this.selectOptions.clear()
this._allSelectOptions
.slice(
(this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
this.selectOptionsPage * SELECT_OPTION_PAGE_SIZE
)
.forEach((option) =>
this.selectOptions.push(
new FormGroup({
label: new FormControl(option.label),
id: new FormControl(option.id),
})
)
)
}
public addSelectOption() { public addSelectOption() {
this.selectOptions.push( this._allSelectOptions.push({ label: null, id: null })
new FormGroup({ label: new FormControl(null), id: new FormControl(null) }) this.selectOptionsPage = Math.ceil(
this.allSelectOptions.length / SELECT_OPTION_PAGE_SIZE
) )
} }
public removeSelectOption(index: number) { public removeSelectOption(index: number) {
this.selectOptions.removeAt(index) this.selectOptions.removeAt(index)
this._allSelectOptions.splice(
index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
1
)
} }
} }

View File

@@ -147,9 +147,13 @@ export abstract class EditDialogComponent<
) )
} }
protected getFormValues(): any {
return Object.assign({}, this.objectForm.value)
}
save() { save() {
this.error = null this.error = null
const formValues = Object.assign({}, this.objectForm.value) const formValues = this.getFormValues()
const permissionsObject: PermissionsFormObject = const permissionsObject: PermissionsFormObject =
this.objectForm.get('permissions_form')?.value this.objectForm.get('permissions_form')?.value
if (permissionsObject) { if (permissionsObject) {

View File

@@ -32,7 +32,7 @@ except ImportError: # pragma: no cover
logger = logging.getLogger("paperless.management.consumer") logger = logging.getLogger("paperless.management.consumer")
def _tags_from_path(filepath) -> list[int]: def _tags_from_path(filepath: Path) -> list[int]:
""" """
Walk up the directory tree from filepath to CONSUMPTION_DIR Walk up the directory tree from filepath to CONSUMPTION_DIR
and get or create Tag IDs for every directory. and get or create Tag IDs for every directory.
@@ -41,7 +41,7 @@ def _tags_from_path(filepath) -> list[int]:
""" """
db.close_old_connections() db.close_old_connections()
tag_ids = set() tag_ids = set()
path_parts = Path(filepath).relative_to(settings.CONSUMPTION_DIR).parent.parts path_parts = filepath.relative_to(settings.CONSUMPTION_DIR).parent.parts
for part in path_parts: for part in path_parts:
tag_ids.add( tag_ids.add(
Tag.objects.get_or_create(name__iexact=part, defaults={"name": part})[0].pk, Tag.objects.get_or_create(name__iexact=part, defaults={"name": part})[0].pk,
@@ -50,17 +50,13 @@ def _tags_from_path(filepath) -> list[int]:
return list(tag_ids) return list(tag_ids)
def _is_ignored(filepath: str) -> bool: def _is_ignored(filepath: Path) -> bool:
""" """
Checks if the given file should be ignored, based on configured Checks if the given file should be ignored, based on configured
patterns. patterns.
Returns True if the file is ignored, False otherwise Returns True if the file is ignored, False otherwise
""" """
filepath = os.path.abspath(
os.path.normpath(filepath),
)
# Trim out the consume directory, leaving only filename and it's # Trim out the consume directory, leaving only filename and it's
# path relative to the consume directory # path relative to the consume directory
filepath_relative = PurePath(filepath).relative_to(settings.CONSUMPTION_DIR) filepath_relative = PurePath(filepath).relative_to(settings.CONSUMPTION_DIR)
@@ -85,15 +81,15 @@ def _is_ignored(filepath: str) -> bool:
return False return False
def _consume(filepath: str) -> None: def _consume(filepath: Path) -> None:
if os.path.isdir(filepath) or _is_ignored(filepath): if filepath.is_dir() or _is_ignored(filepath):
return return
if not os.path.isfile(filepath): if not filepath.is_file():
logger.debug(f"Not consuming file {filepath}: File has moved.") logger.debug(f"Not consuming file {filepath}: File has moved.")
return return
if not is_file_ext_supported(os.path.splitext(filepath)[1]): if not is_file_ext_supported(filepath.suffix):
logger.warning(f"Not consuming file {filepath}: Unknown file extension.") logger.warning(f"Not consuming file {filepath}: Unknown file extension.")
return return
@@ -107,7 +103,7 @@ def _consume(filepath: str) -> None:
while (read_try_count < os_error_retry_count) and not file_open_ok: while (read_try_count < os_error_retry_count) and not file_open_ok:
try: try:
with open(filepath, "rb"): with filepath.open("rb"):
file_open_ok = True file_open_ok = True
except OSError as e: except OSError as e:
read_try_count += 1 read_try_count += 1
@@ -141,7 +137,7 @@ def _consume(filepath: str) -> None:
logger.exception("Error while consuming document") logger.exception("Error while consuming document")
def _consume_wait_unmodified(file: str) -> None: def _consume_wait_unmodified(file: Path) -> None:
""" """
Waits for the given file to appear unmodified based on file size Waits for the given file to appear unmodified based on file size
and modification time. Will wait a configured number of seconds and modification time. Will wait a configured number of seconds
@@ -157,7 +153,7 @@ def _consume_wait_unmodified(file: str) -> None:
current_try = 0 current_try = 0
while current_try < settings.CONSUMER_POLLING_RETRY_COUNT: while current_try < settings.CONSUMER_POLLING_RETRY_COUNT:
try: try:
stat_data = os.stat(file) stat_data = file.stat()
new_mtime = stat_data.st_mtime new_mtime = stat_data.st_mtime
new_size = stat_data.st_size new_size = stat_data.st_size
except FileNotFoundError: except FileNotFoundError:
@@ -182,10 +178,10 @@ class Handler(FileSystemEventHandler):
self._pool = pool self._pool = pool
def on_created(self, event): def on_created(self, event):
self._pool.submit(_consume_wait_unmodified, event.src_path) self._pool.submit(_consume_wait_unmodified, Path(event.src_path))
def on_moved(self, event): def on_moved(self, event):
self._pool.submit(_consume_wait_unmodified, event.dest_path) self._pool.submit(_consume_wait_unmodified, Path(event.dest_path))
class Command(BaseCommand): class Command(BaseCommand):
@@ -227,9 +223,9 @@ class Command(BaseCommand):
if not directory: if not directory:
raise CommandError("CONSUMPTION_DIR does not appear to be set.") raise CommandError("CONSUMPTION_DIR does not appear to be set.")
directory = os.path.abspath(directory) directory = Path(directory).resolve()
if not os.path.isdir(directory): if not directory.is_dir():
raise CommandError(f"Consumption directory {directory} does not exist") raise CommandError(f"Consumption directory {directory} does not exist")
# Consumer will need this # Consumer will need this
@@ -238,11 +234,11 @@ class Command(BaseCommand):
if recursive: if recursive:
for dirpath, _, filenames in os.walk(directory): for dirpath, _, filenames in os.walk(directory):
for filename in filenames: for filename in filenames:
filepath = os.path.join(dirpath, filename) filepath = Path(dirpath) / filename
_consume(filepath) _consume(filepath)
else: else:
for entry in os.scandir(directory): for filepath in directory.iterdir():
_consume(entry.path) _consume(filepath)
if options["oneshot"]: if options["oneshot"]:
return return
@@ -310,7 +306,7 @@ class Command(BaseCommand):
try: try:
for event in inotify.read(timeout=timeout_ms): for event in inotify.read(timeout=timeout_ms):
path = inotify.get_path(event.wd) if recursive else directory path = inotify.get_path(event.wd) if recursive else directory
filepath = os.path.join(path, event.name) filepath = Path(path) / event.name
if flags.MODIFY in flags.from_mask(event.mask): if flags.MODIFY in flags.from_mask(event.mask):
notified_files.pop(filepath, None) notified_files.pop(filepath, None)
else: else:
@@ -327,9 +323,7 @@ class Command(BaseCommand):
# Also make sure the file exists still, some scanners might write a # Also make sure the file exists still, some scanners might write a
# temporary file first # temporary file first
file_still_exists = os.path.exists(filepath) and os.path.isfile( file_still_exists = filepath.exists() and filepath.is_file()
filepath,
)
if waited_long_enough and file_still_exists: if waited_long_enough and file_still_exists:
_consume(filepath) _consume(filepath)

View File

@@ -5,6 +5,7 @@ import logging
import os import os
import shutil import shutil
from collections import defaultdict from collections import defaultdict
from pathlib import Path
from time import sleep from time import sleep
import pathvalidate import pathvalidate
@@ -50,38 +51,38 @@ def many_to_dictionary(field): # pragma: no cover
return mydictionary return mydictionary
def archive_name_from_filename(filename): def archive_name_from_filename(filename: Path) -> Path:
return os.path.splitext(filename)[0] + ".pdf" return Path(filename.stem + ".pdf")
def archive_path_old(doc): def archive_path_old(doc) -> Path:
if doc.filename: if doc.filename:
fname = archive_name_from_filename(doc.filename) fname = archive_name_from_filename(Path(doc.filename))
else: else:
fname = f"{doc.pk:07}.pdf" fname = Path(f"{doc.pk:07}.pdf")
return os.path.join(settings.ARCHIVE_DIR, fname) return settings.ARCHIVE_DIR / fname
STORAGE_TYPE_GPG = "gpg" STORAGE_TYPE_GPG = "gpg"
def archive_path_new(doc): def archive_path_new(doc) -> Path | None:
if doc.archive_filename is not None: if doc.archive_filename is not None:
return os.path.join(settings.ARCHIVE_DIR, str(doc.archive_filename)) return settings.ARCHIVE_DIR / doc.archive_filename
else: else:
return None return None
def source_path(doc): def source_path(doc) -> Path:
if doc.filename: if doc.filename:
fname = str(doc.filename) fname = doc.filename
else: else:
fname = f"{doc.pk:07}{doc.file_type}" fname = f"{doc.pk:07}{doc.file_type}"
if doc.storage_type == STORAGE_TYPE_GPG: if doc.storage_type == STORAGE_TYPE_GPG:
fname += ".gpg" # pragma: no cover fname = Path(str(fname) + ".gpg") # pragma: no cover
return os.path.join(settings.ORIGINALS_DIR, fname) return settings.ORIGINALS_DIR / fname
def generate_unique_filename(doc, *, archive_filename=False): def generate_unique_filename(doc, *, archive_filename=False):
@@ -104,7 +105,7 @@ def generate_unique_filename(doc, *, archive_filename=False):
# still the same as before. # still the same as before.
return new_filename return new_filename
if os.path.exists(os.path.join(root, new_filename)): if (root / new_filename).exists():
counter += 1 counter += 1
else: else:
return new_filename return new_filename
@@ -202,18 +203,18 @@ def create_archive_version(doc, retry_count=3):
parser, parser,
source_path(doc), source_path(doc),
doc.mime_type, doc.mime_type,
os.path.basename(doc.filename), Path(doc.filename).name,
) )
doc.content = parser.get_text() doc.content = parser.get_text()
if parser.get_archive_path() and os.path.isfile(parser.get_archive_path()): if parser.get_archive_path() and Path(parser.get_archive_path()).is_file():
doc.archive_filename = generate_unique_filename( doc.archive_filename = generate_unique_filename(
doc, doc,
archive_filename=True, archive_filename=True,
) )
with open(parser.get_archive_path(), "rb") as f: with Path(parser.get_archive_path()).open("rb") as f:
doc.archive_checksum = hashlib.md5(f.read()).hexdigest() doc.archive_checksum = hashlib.md5(f.read()).hexdigest()
os.makedirs(os.path.dirname(archive_path_new(doc)), exist_ok=True) archive_path_new(doc).parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(parser.get_archive_path(), archive_path_new(doc)) shutil.copy2(parser.get_archive_path(), archive_path_new(doc))
else: else:
doc.archive_checksum = None doc.archive_checksum = None
@@ -264,7 +265,7 @@ def move_old_to_new_locations(apps, schema_editor):
# check that archive files of all unaffected documents are in place # check that archive files of all unaffected documents are in place
for doc in Document.objects.filter(archive_checksum__isnull=False): for doc in Document.objects.filter(archive_checksum__isnull=False):
old_path = archive_path_old(doc) old_path = archive_path_old(doc)
if doc.id not in affected_document_ids and not os.path.isfile(old_path): if doc.id not in affected_document_ids and not old_path.is_file():
raise ValueError( raise ValueError(
f"Archived document ID:{doc.id} does not exist at: {old_path}", f"Archived document ID:{doc.id} does not exist at: {old_path}",
) )
@@ -285,12 +286,12 @@ def move_old_to_new_locations(apps, schema_editor):
if doc.id in affected_document_ids: if doc.id in affected_document_ids:
old_path = archive_path_old(doc) old_path = archive_path_old(doc)
# remove affected archive versions # remove affected archive versions
if os.path.isfile(old_path): if old_path.is_file():
logger.debug(f"Removing {old_path}") logger.debug(f"Removing {old_path}")
os.unlink(old_path) old_path.unlink()
else: else:
# Set archive path for unaffected files # Set archive path for unaffected files
doc.archive_filename = archive_name_from_filename(doc.filename) doc.archive_filename = archive_name_from_filename(Path(doc.filename))
Document.objects.filter(id=doc.id).update( Document.objects.filter(id=doc.id).update(
archive_filename=doc.archive_filename, archive_filename=doc.archive_filename,
) )
@@ -316,7 +317,7 @@ def move_new_to_old_locations(apps, schema_editor):
f"filename.", f"filename.",
) )
old_archive_paths.add(old_archive_path) old_archive_paths.add(old_archive_path)
if new_archive_path != old_archive_path and os.path.isfile(old_archive_path): if new_archive_path != old_archive_path and old_archive_path.is_file():
raise ValueError( raise ValueError(
f"Cannot migrate: Cannot move {new_archive_path} to " f"Cannot migrate: Cannot move {new_archive_path} to "
f"{old_archive_path}: file already exists.", f"{old_archive_path}: file already exists.",

View File

@@ -169,7 +169,7 @@ def run_convert(
args += ["-depth", str(depth)] if depth else [] args += ["-depth", str(depth)] if depth else []
args += ["-auto-orient"] if auto_orient else [] args += ["-auto-orient"] if auto_orient else []
args += ["-define", "pdf:use-cropbox=true"] if use_cropbox else [] args += ["-define", "pdf:use-cropbox=true"] if use_cropbox else []
args += [input_file, output_file] args += [str(input_file), str(output_file)]
logger.debug("Execute: " + " ".join(args), extra={"group": logging_group}) logger.debug("Execute: " + " ".join(args), extra={"group": logging_group})
@@ -188,8 +188,8 @@ def get_default_thumbnail() -> Path:
return (Path(__file__).parent / "resources" / "document.webp").resolve() return (Path(__file__).parent / "resources" / "document.webp").resolve()
def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None) -> str: def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None) -> Path:
out_path = os.path.join(temp_dir, "convert_gs.webp") out_path: Path = Path(temp_dir) / "convert_gs.webp"
# if convert fails, fall back to extracting # if convert fails, fall back to extracting
# the first PDF page as a PNG using Ghostscript # the first PDF page as a PNG using Ghostscript
@@ -199,7 +199,7 @@ def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None) -
extra={"group": logging_group}, extra={"group": logging_group},
) )
# Ghostscript doesn't handle WebP outputs # Ghostscript doesn't handle WebP outputs
gs_out_path = os.path.join(temp_dir, "gs_out.png") gs_out_path: Path = Path(temp_dir) / "gs_out.png"
cmd = [settings.GS_BINARY, "-q", "-sDEVICE=pngalpha", "-o", gs_out_path, in_path] cmd = [settings.GS_BINARY, "-q", "-sDEVICE=pngalpha", "-o", gs_out_path, in_path]
try: try:
@@ -227,16 +227,16 @@ def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None) -
# The caller might expect a generated thumbnail that can be moved, # The caller might expect a generated thumbnail that can be moved,
# so we need to copy it before it gets moved. # so we need to copy it before it gets moved.
# https://github.com/paperless-ngx/paperless-ngx/issues/3631 # https://github.com/paperless-ngx/paperless-ngx/issues/3631
default_thumbnail_path = os.path.join(temp_dir, "document.webp") default_thumbnail_path: Path = Path(temp_dir) / "document.webp"
copy_file_with_basic_stats(get_default_thumbnail(), default_thumbnail_path) copy_file_with_basic_stats(get_default_thumbnail(), default_thumbnail_path)
return default_thumbnail_path return default_thumbnail_path
def make_thumbnail_from_pdf(in_path, temp_dir, logging_group=None) -> Path: def make_thumbnail_from_pdf(in_path: Path, temp_dir: Path, logging_group=None) -> Path:
""" """
The thumbnail of a PDF is just a 500px wide image of the first page. The thumbnail of a PDF is just a 500px wide image of the first page.
""" """
out_path = temp_dir / "convert.webp" out_path: Path = temp_dir / "convert.webp"
# Run convert to get a decent thumbnail # Run convert to get a decent thumbnail
try: try:

File diff suppressed because one or more lines are too long

View File

@@ -654,7 +654,7 @@ class TestClassifier(DirectoriesMixin, TestCase):
}, },
) )
@override_settings( @override_settings(
MODEL_FILE=(Path(__file__).parent / "data" / "model.pickle").as_posix(), MODEL_FILE=str(Path(__file__).parent / "data" / "model.pickle"),
) )
@pytest.mark.skip( @pytest.mark.skip(
reason="Disabled caching due to high memory usage - need to investigate.", reason="Disabled caching due to high memory usage - need to investigate.",

View File

@@ -254,7 +254,7 @@ class TestConsumer(
# https://github.com/jonaswinkler/paperless-ng/discussions/1037 # https://github.com/jonaswinkler/paperless-ng/discussions/1037
filename = self.get_test_file() filename = self.get_test_file()
shadow_file = Path(self.dirs.scratch_dir / "._sample.pdf") shadow_file = Path(self.dirs.scratch_dir) / "._sample.pdf"
shutil.copy(filename, shadow_file) shutil.copy(filename, shadow_file)

View File

@@ -258,66 +258,66 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
def test_is_ignored(self): def test_is_ignored(self):
test_paths = [ test_paths = [
{ {
"path": (Path(self.dirs.consumption_dir) / "foo.pdf").as_posix(), "path": str(Path(self.dirs.consumption_dir) / "foo.pdf"),
"ignore": False, "ignore": False,
}, },
{ {
"path": ( "path": str(
Path(self.dirs.consumption_dir) / "foo" / "bar.pdf" Path(self.dirs.consumption_dir) / "foo" / "bar.pdf",
).as_posix(), ),
"ignore": False, "ignore": False,
}, },
{ {
"path": (Path(self.dirs.consumption_dir) / ".DS_STORE").as_posix(), "path": str(Path(self.dirs.consumption_dir) / ".DS_STORE"),
"ignore": True, "ignore": True,
}, },
{ {
"path": (Path(self.dirs.consumption_dir) / ".DS_Store").as_posix(), "path": str(Path(self.dirs.consumption_dir) / ".DS_Store"),
"ignore": True, "ignore": True,
}, },
{ {
"path": ( "path": str(
Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf" Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf",
).as_posix(), ),
"ignore": True, "ignore": True,
}, },
{ {
"path": (Path(self.dirs.consumption_dir) / ".stfolder.pdf").as_posix(), "path": str(Path(self.dirs.consumption_dir) / ".stfolder.pdf"),
"ignore": False, "ignore": False,
}, },
{ {
"path": ( "path": str(
Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf" Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf",
).as_posix(), ),
"ignore": True, "ignore": True,
}, },
{ {
"path": ( "path": str(
Path(self.dirs.consumption_dir) / ".stversions.pdf" Path(self.dirs.consumption_dir) / ".stversions.pdf",
).as_posix(), ),
"ignore": False, "ignore": False,
}, },
{ {
"path": (Path(self.dirs.consumption_dir) / "._foo.pdf").as_posix(), "path": str(Path(self.dirs.consumption_dir) / "._foo.pdf"),
"ignore": True, "ignore": True,
}, },
{ {
"path": (Path(self.dirs.consumption_dir) / "my_foo.pdf").as_posix(), "path": str(Path(self.dirs.consumption_dir) / "my_foo.pdf"),
"ignore": False, "ignore": False,
}, },
{ {
"path": ( "path": str(
Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf" Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf",
).as_posix(), ),
"ignore": True, "ignore": True,
}, },
{ {
"path": ( "path": str(
Path(self.dirs.consumption_dir) Path(self.dirs.consumption_dir)
/ "@eaDir" / "@eaDir"
/ "SYNO@.fileindexdb" / "SYNO@.fileindexdb"
/ "_1jk.fnm" / "_1jk.fnm",
).as_posix(), ),
"ignore": True, "ignore": True,
}, },
] ]
@@ -330,7 +330,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
f'_is_ignored("{filepath}") != {expected_ignored_result}', f'_is_ignored("{filepath}") != {expected_ignored_result}',
) )
@mock.patch("documents.management.commands.document_consumer.open") @mock.patch("documents.management.commands.document_consumer.Path.open")
def test_consume_file_busy(self, open_mock): def test_consume_file_busy(self, open_mock):
# Calling this mock always raises this # Calling this mock always raises this
open_mock.side_effect = OSError open_mock.side_effect = OSError

View File

@@ -230,9 +230,9 @@ class TestExportImport(
for element in manifest: for element in manifest:
if element["model"] == "documents.document": if element["model"] == "documents.document":
fname = ( fname = str(
self.target / element[document_exporter.EXPORTER_FILE_NAME] self.target / element[document_exporter.EXPORTER_FILE_NAME],
).as_posix() )
self.assertIsFile(fname) self.assertIsFile(fname)
self.assertIsFile( self.assertIsFile(
self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME], self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME],
@@ -462,9 +462,9 @@ class TestExportImport(
call_command(*args) call_command(*args)
expected_file = ( expected_file = str(
self.target / f"export-{timezone.localdate().isoformat()}.zip" self.target / f"export-{timezone.localdate().isoformat()}.zip",
).as_posix() )
self.assertIsFile(expected_file) self.assertIsFile(expected_file)
@@ -498,9 +498,9 @@ class TestExportImport(
): ):
call_command(*args) call_command(*args)
expected_file = ( expected_file = str(
self.target / f"export-{timezone.localdate().isoformat()}.zip" self.target / f"export-{timezone.localdate().isoformat()}.zip",
).as_posix() )
self.assertIsFile(expected_file) self.assertIsFile(expected_file)
@@ -544,9 +544,9 @@ class TestExportImport(
call_command(*args) call_command(*args)
expected_file = ( expected_file = str(
self.target / f"export-{timezone.localdate().isoformat()}.zip" self.target / f"export-{timezone.localdate().isoformat()}.zip",
).as_posix() )
self.assertIsFile(expected_file) self.assertIsFile(expected_file)
self.assertIsNotFile(existing_file) self.assertIsNotFile(existing_file)

View File

@@ -19,15 +19,15 @@ migration_1012_obj = importlib.import_module(
) )
def archive_name_from_filename(filename): def archive_name_from_filename(filename: Path) -> Path:
return Path(filename).stem + ".pdf" return Path(filename.stem + ".pdf")
def archive_path_old(self): def archive_path_old(self) -> Path:
if self.filename: if self.filename:
fname = archive_name_from_filename(self.filename) fname = archive_name_from_filename(Path(self.filename))
else: else:
fname = f"{self.pk:07}.pdf" fname = Path(f"{self.pk:07}.pdf")
return Path(settings.ARCHIVE_DIR) / fname return Path(settings.ARCHIVE_DIR) / fname

View File

@@ -2899,7 +2899,7 @@ class SystemStatusView(PassUserMixin):
install_type = "docker" install_type = "docker"
db_conn = connections["default"] db_conn = connections["default"]
db_url = db_conn.settings_dict["NAME"] db_url = str(db_conn.settings_dict["NAME"])
db_error = None db_error = None
try: try:

View File

@@ -711,7 +711,7 @@ def _parse_db_settings() -> dict:
databases = { databases = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.sqlite3",
"NAME": str(DATA_DIR / "db.sqlite3"), "NAME": DATA_DIR / "db.sqlite3",
"OPTIONS": {}, "OPTIONS": {},
}, },
} }
@@ -839,7 +839,7 @@ LANGUAGES = [
("zh-tw", _("Chinese Traditional")), ("zh-tw", _("Chinese Traditional")),
] ]
LOCALE_PATHS = [str(BASE_DIR / "locale")] LOCALE_PATHS = [BASE_DIR / "locale"]
TIME_ZONE = os.getenv("PAPERLESS_TIME_ZONE", "UTC") TIME_ZONE = os.getenv("PAPERLESS_TIME_ZONE", "UTC")
@@ -880,21 +880,21 @@ LOGGING = {
"file_paperless": { "file_paperless": {
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler", "class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
"formatter": "verbose", "formatter": "verbose",
"filename": str(LOGGING_DIR / "paperless.log"), "filename": LOGGING_DIR / "paperless.log",
"maxBytes": LOGROTATE_MAX_SIZE, "maxBytes": LOGROTATE_MAX_SIZE,
"backupCount": LOGROTATE_MAX_BACKUPS, "backupCount": LOGROTATE_MAX_BACKUPS,
}, },
"file_mail": { "file_mail": {
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler", "class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
"formatter": "verbose", "formatter": "verbose",
"filename": str(LOGGING_DIR / "mail.log"), "filename": LOGGING_DIR / "mail.log",
"maxBytes": LOGROTATE_MAX_SIZE, "maxBytes": LOGROTATE_MAX_SIZE,
"backupCount": LOGROTATE_MAX_BACKUPS, "backupCount": LOGROTATE_MAX_BACKUPS,
}, },
"file_celery": { "file_celery": {
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler", "class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
"formatter": "verbose", "formatter": "verbose",
"filename": str(LOGGING_DIR / "celery.log"), "filename": LOGGING_DIR / "celery.log",
"maxBytes": LOGROTATE_MAX_SIZE, "maxBytes": LOGROTATE_MAX_SIZE,
"backupCount": LOGROTATE_MAX_BACKUPS, "backupCount": LOGROTATE_MAX_BACKUPS,
}, },
@@ -954,7 +954,7 @@ CELERY_ACCEPT_CONTENT = ["application/json", "application/x-python-serialize"]
CELERY_BEAT_SCHEDULE = _parse_beat_schedule() CELERY_BEAT_SCHEDULE = _parse_beat_schedule()
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-schedule-filename # https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-schedule-filename
CELERY_BEAT_SCHEDULE_FILENAME = str(DATA_DIR / "celerybeat-schedule.db") CELERY_BEAT_SCHEDULE_FILENAME = DATA_DIR / "celerybeat-schedule.db"
# Cachalot: Database read cache. # Cachalot: Database read cache.

View File

@@ -69,13 +69,13 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(uuid.uuid4()) parser = RasterisedDocumentParser(uuid.uuid4())
page_count = parser.get_page_count( page_count = parser.get_page_count(
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "simple-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertEqual(page_count, 1) self.assertEqual(page_count, 1)
page_count = parser.get_page_count( page_count = parser.get_page_count(
(self.SAMPLE_FILES / "multi-page-mixed.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-mixed.pdf"),
"application/pdf", "application/pdf",
) )
self.assertEqual(page_count, 6) self.assertEqual(page_count, 6)
@@ -92,7 +92,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(uuid.uuid4()) parser = RasterisedDocumentParser(uuid.uuid4())
with self.assertLogs("paperless.parsing.tesseract", level="WARNING") as cm: with self.assertLogs("paperless.parsing.tesseract", level="WARNING") as cm:
page_count = parser.get_page_count( page_count = parser.get_page_count(
(self.SAMPLE_FILES / "password-protected.pdf").as_posix(), str(self.SAMPLE_FILES / "password-protected.pdf"),
"application/pdf", "application/pdf",
) )
self.assertEqual(page_count, None) self.assertEqual(page_count, None)
@@ -101,7 +101,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_thumbnail(self): def test_thumbnail(self):
parser = RasterisedDocumentParser(uuid.uuid4()) parser = RasterisedDocumentParser(uuid.uuid4())
thumb = parser.get_thumbnail( thumb = parser.get_thumbnail(
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "simple-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(thumb) self.assertIsFile(thumb)
@@ -109,7 +109,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
@mock.patch("documents.parsers.run_convert") @mock.patch("documents.parsers.run_convert")
def test_thumbnail_fallback(self, m): def test_thumbnail_fallback(self, m):
def call_convert(input_file, output_file, **kwargs): def call_convert(input_file, output_file, **kwargs):
if ".pdf" in input_file: if ".pdf" in str(input_file):
raise ParseError("Does not compute.") raise ParseError("Does not compute.")
else: else:
run_convert(input_file=input_file, output_file=output_file, **kwargs) run_convert(input_file=input_file, output_file=output_file, **kwargs)
@@ -118,7 +118,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(uuid.uuid4()) parser = RasterisedDocumentParser(uuid.uuid4())
thumb = parser.get_thumbnail( thumb = parser.get_thumbnail(
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "simple-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(thumb) self.assertIsFile(thumb)
@@ -126,7 +126,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_thumbnail_encrypted(self): def test_thumbnail_encrypted(self):
parser = RasterisedDocumentParser(uuid.uuid4()) parser = RasterisedDocumentParser(uuid.uuid4())
thumb = parser.get_thumbnail( thumb = parser.get_thumbnail(
(self.SAMPLE_FILES / "encrypted.pdf").as_posix(), str(self.SAMPLE_FILES / "encrypted.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(thumb) self.assertIsFile(thumb)
@@ -134,17 +134,17 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_get_dpi(self): def test_get_dpi(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
dpi = parser.get_dpi((self.SAMPLE_FILES / "simple-no-dpi.png").as_posix()) dpi = parser.get_dpi(str(self.SAMPLE_FILES / "simple-no-dpi.png"))
self.assertEqual(dpi, None) self.assertEqual(dpi, None)
dpi = parser.get_dpi((self.SAMPLE_FILES / "simple.png").as_posix()) dpi = parser.get_dpi(str(self.SAMPLE_FILES / "simple.png"))
self.assertEqual(dpi, 72) self.assertEqual(dpi, 72)
def test_simple_digital(self): def test_simple_digital(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "simple-digital.pdf"),
"application/pdf", "application/pdf",
) )
@@ -156,7 +156,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "with-form.pdf").as_posix(), str(self.SAMPLE_FILES / "with-form.pdf"),
"application/pdf", "application/pdf",
) )
@@ -172,7 +172,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "with-form.pdf").as_posix(), str(self.SAMPLE_FILES / "with-form.pdf"),
"application/pdf", "application/pdf",
) )
@@ -186,7 +186,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_signed(self): def test_signed(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "signed.pdf").as_posix(), "application/pdf") parser.parse(str(self.SAMPLE_FILES / "signed.pdf"), "application/pdf")
self.assertIsNone(parser.archive_path) self.assertIsNone(parser.archive_path)
self.assertContainsStrings( self.assertContainsStrings(
@@ -202,7 +202,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "encrypted.pdf").as_posix(), str(self.SAMPLE_FILES / "encrypted.pdf"),
"application/pdf", "application/pdf",
) )
@@ -213,7 +213,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_with_form_error_notext(self): def test_with_form_error_notext(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "with-form.pdf").as_posix(), str(self.SAMPLE_FILES / "with-form.pdf"),
"application/pdf", "application/pdf",
) )
@@ -227,7 +227,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "with-form.pdf").as_posix(), str(self.SAMPLE_FILES / "with-form.pdf"),
"application/pdf", "application/pdf",
) )
@@ -239,7 +239,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_image_simple(self): def test_image_simple(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple.png").as_posix(), "image/png") parser.parse(str(self.SAMPLE_FILES / "simple.png"), "image/png")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -255,7 +255,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
dest_file = Path(tempdir) / "simple-alpha.png" dest_file = Path(tempdir) / "simple-alpha.png"
shutil.copy(sample_file, dest_file) shutil.copy(sample_file, dest_file)
parser.parse(dest_file.as_posix(), "image/png") parser.parse(str(dest_file), "image/png")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -265,7 +265,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
dpi = parser.calculate_a4_dpi( dpi = parser.calculate_a4_dpi(
(self.SAMPLE_FILES / "simple-no-dpi.png").as_posix(), str(self.SAMPLE_FILES / "simple-no-dpi.png"),
) )
self.assertEqual(dpi, 62) self.assertEqual(dpi, 62)
@@ -277,7 +277,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def f(): def f():
parser.parse( parser.parse(
(self.SAMPLE_FILES / "simple-no-dpi.png").as_posix(), str(self.SAMPLE_FILES / "simple-no-dpi.png"),
"image/png", "image/png",
) )
@@ -287,7 +287,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_image_no_dpi_default(self): def test_image_no_dpi_default(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple-no-dpi.png").as_posix(), "image/png") parser.parse(str(self.SAMPLE_FILES / "simple-no-dpi.png"), "image/png")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -299,7 +299,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_multi_page(self): def test_multi_page(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -312,7 +312,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_multi_page_pages_skip(self): def test_multi_page_pages_skip(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -325,7 +325,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_multi_page_pages_redo(self): def test_multi_page_pages_redo(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -338,7 +338,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_multi_page_pages_force(self): def test_multi_page_pages_force(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -351,7 +351,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_multi_page_analog_pages_skip(self): def test_multi_page_analog_pages_skip(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -375,7 +375,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -397,7 +397,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -419,7 +419,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNone(parser.archive_path) self.assertIsNone(parser.archive_path)
@@ -442,7 +442,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
@@ -467,7 +467,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNotNone(parser.archive_path) self.assertIsNotNone(parser.archive_path)
@@ -490,7 +490,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNotNone(parser.archive_path) self.assertIsNotNone(parser.archive_path)
@@ -513,7 +513,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNone(parser.archive_path) self.assertIsNone(parser.archive_path)
@@ -536,7 +536,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNotNone(parser.archive_path) self.assertIsNotNone(parser.archive_path)
@@ -559,7 +559,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNone(parser.archive_path) self.assertIsNone(parser.archive_path)
@@ -582,7 +582,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNone(parser.archive_path) self.assertIsNone(parser.archive_path)
@@ -605,7 +605,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-mixed.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-mixed.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNotNone(parser.archive_path) self.assertIsNotNone(parser.archive_path)
@@ -636,7 +636,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "single-page-mixed.pdf").as_posix(), str(self.SAMPLE_FILES / "single-page-mixed.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNotNone(parser.archive_path) self.assertIsNotNone(parser.archive_path)
@@ -673,7 +673,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-mixed.pdf").as_posix(), str(self.SAMPLE_FILES / "multi-page-mixed.pdf"),
"application/pdf", "application/pdf",
) )
self.assertIsNone(parser.archive_path) self.assertIsNone(parser.archive_path)
@@ -685,7 +685,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
@override_settings(OCR_MODE="skip", OCR_ROTATE_PAGES=True) @override_settings(OCR_MODE="skip", OCR_ROTATE_PAGES=True)
def test_rotate(self): def test_rotate(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "rotated.pdf").as_posix(), "application/pdf") parser.parse(str(self.SAMPLE_FILES / "rotated.pdf"), "application/pdf")
self.assertContainsStrings( self.assertContainsStrings(
parser.get_text(), parser.get_text(),
[ [
@@ -707,7 +707,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "multi-page-images.tiff").as_posix(), str(self.SAMPLE_FILES / "multi-page-images.tiff"),
"image/tiff", "image/tiff",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
@@ -752,9 +752,9 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
- Text from all pages extracted - Text from all pages extracted
""" """
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
sample_file = ( sample_file = str(
self.SAMPLE_FILES / "multi-page-images-alpha-rgb.tiff" self.SAMPLE_FILES / "multi-page-images-alpha-rgb.tiff",
).as_posix() )
with tempfile.NamedTemporaryFile() as tmp_file: with tempfile.NamedTemporaryFile() as tmp_file:
shutil.copy(sample_file, tmp_file.name) shutil.copy(sample_file, tmp_file.name)
parser.parse( parser.parse(
@@ -843,7 +843,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "rtl-test.pdf").as_posix(), str(self.SAMPLE_FILES / "rtl-test.pdf"),
"application/pdf", "application/pdf",
) )
@@ -858,7 +858,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertRaises( self.assertRaises(
ParseError, ParseError,
parser.parse, parser.parse,
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(), str(self.SAMPLE_FILES / "simple-digital.pdf"),
"application/pdf", "application/pdf",
) )
@@ -868,32 +868,32 @@ class TestParserFileTypes(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_bmp(self): def test_bmp(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple.bmp").as_posix(), "image/bmp") parser.parse(str(self.SAMPLE_FILES / "simple.bmp"), "image/bmp")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
self.assertIn("this is a test document", parser.get_text().lower()) self.assertIn("this is a test document", parser.get_text().lower())
def test_jpg(self): def test_jpg(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple.jpg").as_posix(), "image/jpeg") parser.parse(str(self.SAMPLE_FILES / "simple.jpg"), "image/jpeg")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
self.assertIn("this is a test document", parser.get_text().lower()) self.assertIn("this is a test document", parser.get_text().lower())
def test_heic(self): def test_heic(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple.heic").as_posix(), "image/heic") parser.parse(str(self.SAMPLE_FILES / "simple.heic"), "image/heic")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
self.assertIn("pizza", parser.get_text().lower()) self.assertIn("pizza", parser.get_text().lower())
@override_settings(OCR_IMAGE_DPI=200) @override_settings(OCR_IMAGE_DPI=200)
def test_gif(self): def test_gif(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple.gif").as_posix(), "image/gif") parser.parse(str(self.SAMPLE_FILES / "simple.gif"), "image/gif")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
self.assertIn("this is a test document", parser.get_text().lower()) self.assertIn("this is a test document", parser.get_text().lower())
def test_tiff(self): def test_tiff(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse((self.SAMPLE_FILES / "simple.tif").as_posix(), "image/tiff") parser.parse(str(self.SAMPLE_FILES / "simple.tif"), "image/tiff")
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)
self.assertIn("this is a test document", parser.get_text().lower()) self.assertIn("this is a test document", parser.get_text().lower())
@@ -901,7 +901,7 @@ class TestParserFileTypes(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_webp(self): def test_webp(self):
parser = RasterisedDocumentParser(None) parser = RasterisedDocumentParser(None)
parser.parse( parser.parse(
(self.SAMPLE_FILES / "document.webp").as_posix(), str(self.SAMPLE_FILES / "document.webp"),
"image/webp", "image/webp",
) )
self.assertIsFile(parser.archive_path) self.assertIsFile(parser.archive_path)