mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-05 02:06:20 +00:00
Compare commits
14 Commits
v2.18.3
...
release-wo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3cfb0f5856 | ||
![]() |
a79c8dc51c | ||
![]() |
4b95c2f0e5 | ||
![]() |
e1c8cd779b | ||
![]() |
cc7c7f31ba | ||
![]() |
1d30ce2afa | ||
![]() |
5aa86f8755 | ||
![]() |
de2ddad5ee | ||
![]() |
d2064a2535 | ||
![]() |
cc621cf729 | ||
![]() |
fc4134e15c | ||
![]() |
ac1b420966 | ||
![]() |
80595899c1 | ||
![]() |
9463a8fd26 |
@@ -3,7 +3,7 @@
|
||||
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
|
||||
"service": "paperless-development",
|
||||
"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": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Start containers
|
||||
run: |
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
needs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
shard-index: [1, 2, 3, 4]
|
||||
shard-count: [4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
shard-index: [1, 2]
|
||||
shard-count: [2]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
- tests-frontend
|
||||
- tests-frontend-e2e
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -363,7 +363,7 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
# If https://github.com/docker/buildx/issues/1044 is resolved,
|
||||
# the append input with a native arm64 arch could be used to
|
||||
# significantly speed up building
|
||||
@@ -433,7 +433,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -453,12 +453,12 @@ jobs:
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -qq --no-install-recommends gettext liblept5
|
||||
- name: Download frontend artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
- name: Download documentation artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: documentation
|
||||
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'))
|
||||
steps:
|
||||
- name: Download release artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: release
|
||||
path: ./
|
||||
@@ -579,7 +579,7 @@ jobs:
|
||||
if: needs.publish-release.outputs.prerelease == 'false'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
- name: Set up Python
|
||||
|
4
.github/workflows/cleanup-tags.yml
vendored
4
.github/workflows/cleanup-tags.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.10.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.11.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.10.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.11.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
2
.github/workflows/crowdin.yml
vendored
2
.github/workflows/crowdin.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||
- name: crowdin action
|
||||
|
2
.github/workflows/pr-bot.yml
vendored
2
.github/workflows/pr-bot.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
labels.push('bug');
|
||||
} else if (/^feature/i.test(title)) {
|
||||
labels.push('enhancement');
|
||||
} else if (!/^(dependabot)/i.test(title)) {
|
||||
} else if (!/^(dependabot)/i.test(title) && /^(chore)/i.test(title)) {
|
||||
labels.push('enhancement'); // Default fallback
|
||||
}
|
||||
|
||||
|
135
.github/workflows/release.yml
vendored
Normal file
135
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: Paperless-ngx Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version (e.g., 2.18.3)"
|
||||
required: true
|
||||
type: string
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
concurrency:
|
||||
group: release-main
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout (full)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "${{ github.actor }}"
|
||||
git config user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
- name: Sanitize & validate input
|
||||
id: ver
|
||||
shell: bash
|
||||
run: |
|
||||
RAW="${{ github.event.inputs.version }}"
|
||||
# trim spaces + strip leading 'v' if present
|
||||
RAW="${RAW//[[:space:]]/}"
|
||||
RAW="${RAW#v}"
|
||||
|
||||
# basic semver X.Y.Z
|
||||
if [[ ! "$RAW" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ Invalid version: '$RAW' (expected X.Y.Z or vX.Y.Z)"; exit 1
|
||||
fi
|
||||
|
||||
MAJOR="${RAW%%.*}"
|
||||
REST="${RAW#*.}"
|
||||
MINOR="${REST%%.*}"
|
||||
PATCH="${REST#*.}"
|
||||
|
||||
echo "version=$RAW" >> "$GITHUB_OUTPUT"
|
||||
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
||||
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
||||
echo "patch=$PATCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "✅ Using version $RAW"
|
||||
- name: Ensure tag does not already exist
|
||||
run: |
|
||||
git fetch --tags
|
||||
if git rev-parse "v${{ steps.ver.outputs.version }}" >/dev/null 2>&1; then
|
||||
echo "❌ Tag v${{ steps.ver.outputs.version }} already exists"; exit 1
|
||||
fi
|
||||
- name: Update local branches
|
||||
run: |
|
||||
git fetch origin main dev
|
||||
- name: Fast-forward main to dev (no merge commits)
|
||||
run: |
|
||||
# Reset local main to remote, then try fast-forward to dev.
|
||||
git checkout main
|
||||
git reset --hard origin/main
|
||||
# --ff-only ensures the workflow fails if the branches diverged.
|
||||
git merge --ff-only origin/dev
|
||||
echo "✅ main fast-forwarded to dev at $(git rev-parse --short HEAD)"
|
||||
- name: Bump versions in files
|
||||
shell: bash
|
||||
run: |
|
||||
VER="${{ steps.ver.outputs.version }}"
|
||||
MAJ="${{ steps.ver.outputs.major }}"
|
||||
MIN="${{ steps.ver.outputs.minor }}"
|
||||
PAT="${{ steps.ver.outputs.patch }}"
|
||||
|
||||
# 1) pyproject.toml: [project] version = "X.Y.Z"
|
||||
sed -i -E 's/^version = "[0-9]+\.[0-9]+\.[0-9]+"/version = "'"$VER"'"/' pyproject.toml
|
||||
|
||||
# 2) src-ui/package.json: "version": "X.Y.Z"
|
||||
# Use jq if available; otherwise sed fallback.
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
tmp=$(mktemp)
|
||||
jq --arg v "$VER" '.version=$v' src-ui/package.json > "$tmp" && mv "$tmp" src-ui/package.json
|
||||
else
|
||||
sed -i -E 's/"version": "[0-9]+\.[0-9]+\.[0-9]+"/"version": "'"$VER"'"/' src-ui/package.json
|
||||
fi
|
||||
|
||||
# 3) src-ui/src/environments/environment.prod.ts: version: 'X.Y.Z'
|
||||
sed -i -E "s/version: '[0-9]+\.[0-9]+\.[0-9]+'/version: '$VER'/" src-ui/src/environments/environment.prod.ts
|
||||
|
||||
# 4) src/paperless/version.py: __version__ = (X, Y, Z)
|
||||
sed -i -E "s/__version__:\s*Final\[tuple\[int,\s*int,\s*int\]\]\s*=\s*\([0-9]+,\s*[0-9]+,\s*[0-9]+\)/__version__: Final[tuple[int, int, int]] = ($MAJ, $MIN, $PAT)/" src/paperless/version.py
|
||||
|
||||
# 5) uv.lock: in the [[package]] name = "paperless-ngx" block, set version = "X.Y.Z"
|
||||
# This awk edits only the block for paperless-ngx.
|
||||
awk -v ver="$VER" '
|
||||
BEGIN{inpkg=0}
|
||||
/^\[\[package\]\]/{inpkg=0}
|
||||
/^\[\[package\]\]/{print; next}
|
||||
{print > "/dev/stdout"}
|
||||
' uv.lock >/dev/null 2>&1 # noop to ensure awk exists
|
||||
|
||||
# More robust in-place edit with awk:
|
||||
awk -v ver="$VER" '
|
||||
BEGIN{inpkg=0}
|
||||
/^\[\[package\]\]/{inpkg=0; print; next}
|
||||
/^name = "paperless-ngx"/{inpkg=1; print; next}
|
||||
inpkg && /^version = "/{
|
||||
sub(/version = "[0-9]+\.[0-9]+\.[0-9]+"/, "version = \"" ver "\"")
|
||||
print; next
|
||||
}
|
||||
{print}
|
||||
' uv.lock > uv.lock.new && mv uv.lock.new uv.lock
|
||||
|
||||
echo "✅ Files updated to $VER"
|
||||
- name: Commit bump (if changes)
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "ℹ️ No changes to commit (versions may already match)";
|
||||
else
|
||||
git add pyproject.toml src-ui/package.json src-ui/src/environments/environment.prod.ts src/paperless/version.py uv.lock
|
||||
git commit -m "Bump version to ${{ steps.ver.outputs.version }}"
|
||||
fi
|
||||
- name: Push main
|
||||
run: |
|
||||
# Push branch (even if no commit, ensures remote main == local)
|
||||
git push origin HEAD:main
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
VER="${{ steps.ver.outputs.version }}"
|
||||
git tag -a "v${VER}" -m "Release v${VER}"
|
||||
git push origin "v${VER}"
|
||||
- name: Done
|
||||
run: echo "🎉 Release v${{ steps.ver.outputs.version }} created and pushed."
|
2
.github/workflows/translate-strings.yml
vendored
2
.github/workflows/translate-strings.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
@@ -18,7 +18,7 @@ repos:
|
||||
exclude_types:
|
||||
- svg
|
||||
- pofile
|
||||
exclude: "(^LICENSE$)"
|
||||
exclude: "(^LICENSE$|^src/documents/static/bootstrap.min.css$)"
|
||||
- id: mixed-line-ending
|
||||
args:
|
||||
- "--fix=lf"
|
||||
@@ -51,7 +51,7 @@ repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "v2.6.0"
|
||||
|
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.18.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: include application config language settings for dateparser auto-detection [@shamoon](https://github.com/shamoon) ([#10722](https://github.com/paperless-ngx/paperless-ngx/pull/10722))
|
||||
- Fix: hide sidebar counts during saved views organization [@shamoon](https://github.com/shamoon) ([#10716](https://github.com/paperless-ngx/paperless-ngx/pull/10716))
|
||||
- Fix: wrap long view titles in sidebar [@shamoon](https://github.com/shamoon) ([#10715](https://github.com/paperless-ngx/paperless-ngx/pull/10715))
|
||||
- Fixhancement: more saved view count refreshes [@shamoon](https://github.com/shamoon) ([#10694](https://github.com/paperless-ngx/paperless-ngx/pull/10694))
|
||||
- Fix: include pagination array items for valid openapi schema [@shamoon](https://github.com/shamoon) ([#10682](https://github.com/paperless-ngx/paperless-ngx/pull/10682))
|
||||
- Fix: prevent scroll for view name in sidebar [@shamoon](https://github.com/shamoon) ([#10676](https://github.com/paperless-ngx/paperless-ngx/pull/10676))
|
||||
- Tweak: center document close button in app frame [@shamoon](https://github.com/shamoon) ([#10661](https://github.com/paperless-ngx/paperless-ngx/pull/10661))
|
||||
- Performance: Enable virtual scrolling for large custom field selects @david-loe ([#10708](https://github.com/paperless-ngx/paperless-ngx/pull/10708))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Chore(deps): Update granian[uvloop] requirement from ~=2.4.1 to ~=2.5.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#10529](https://github.com/paperless-ngx/paperless-ngx/pull/10529))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#10714](https://github.com/paperless-ngx/paperless-ngx/pull/10714))
|
||||
- docker-compose(deps): Bump library/mariadb from 11 to 12 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#10621](https://github.com/paperless-ngx/paperless-ngx/pull/10621))
|
||||
- docker-compose(deps): Bump gotenberg/gotenberg from 8.20 to 8.22 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#10687](https://github.com/paperless-ngx/paperless-ngx/pull/10687))
|
||||
- docker(deps): Bump astral-sh/uv from 0.8.8-python3.12-bookworm-slim to 0.8.13-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#10685](https://github.com/paperless-ngx/paperless-ngx/pull/10685))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>11 changes</summary>
|
||||
|
||||
- Fix: include application config language settings for dateparser auto-detection [@shamoon](https://github.com/shamoon) ([#10722](https://github.com/paperless-ngx/paperless-ngx/pull/10722))
|
||||
- Chore(deps): Update granian[uvloop] requirement from ~=2.4.1 to ~=2.5.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#10529](https://github.com/paperless-ngx/paperless-ngx/pull/10529))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#10714](https://github.com/paperless-ngx/paperless-ngx/pull/10714))
|
||||
- Fix: hide sidebar counts during saved views organization [@shamoon](https://github.com/shamoon) ([#10716](https://github.com/paperless-ngx/paperless-ngx/pull/10716))
|
||||
- Fix: wrap long view titles in sidebar [@shamoon](https://github.com/shamoon) ([#10715](https://github.com/paperless-ngx/paperless-ngx/pull/10715))
|
||||
- Performance: Enable virtual scrolling for large custom field selects @david-loe ([#10708](https://github.com/paperless-ngx/paperless-ngx/pull/10708))
|
||||
- Chore: refactor document details component [@shamoon](https://github.com/shamoon) ([#10662](https://github.com/paperless-ngx/paperless-ngx/pull/10662))
|
||||
- Fixhancement: more saved view count refreshes [@shamoon](https://github.com/shamoon) ([#10694](https://github.com/paperless-ngx/paperless-ngx/pull/10694))
|
||||
- Fix: include pagination array items for valid openapi schema [@shamoon](https://github.com/shamoon) ([#10682](https://github.com/paperless-ngx/paperless-ngx/pull/10682))
|
||||
- Fix: prevent scroll for view name in sidebar [@shamoon](https://github.com/shamoon) ([#10676](https://github.com/paperless-ngx/paperless-ngx/pull/10676))
|
||||
- Tweak: center document close button in app frame [@shamoon](https://github.com/shamoon) ([#10661](https://github.com/paperless-ngx/paperless-ngx/pull/10661))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.18.2
|
||||
|
||||
### Bug Fixes
|
||||
|
@@ -205,18 +205,9 @@ lint.per-file-ignores."docker/wait-for-redis.py" = [
|
||||
"INP001",
|
||||
"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" = [
|
||||
"SIM115",
|
||||
]
|
||||
lint.per-file-ignores."src/documents/parsers.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
||||
"RUF001",
|
||||
]
|
||||
|
@@ -5,14 +5,14 @@
|
||||
<trans-unit id="ngb.alert.close" datatype="html">
|
||||
<source>Close</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.carousel.slide-number" datatype="html">
|
||||
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
|
||||
<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-group>
|
||||
<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">
|
||||
<source>Previous</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.carousel.next" datatype="html">
|
||||
<source>Next</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.datepicker.previous-month" datatype="html">
|
||||
<source>Previous month</source>
|
||||
<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-group>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.datepicker.next-month" datatype="html">
|
||||
<source>Next month</source>
|
||||
<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-group>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.HH" datatype="html">
|
||||
<source>HH</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.toast.close-aria" datatype="html">
|
||||
<source>Close</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.datepicker.select-month" datatype="html">
|
||||
<source>Select month</source>
|
||||
<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-group>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.first" datatype="html">
|
||||
<source>««</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.hours" datatype="html">
|
||||
<source>Hours</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.previous" datatype="html">
|
||||
<source>«</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.MM" datatype="html">
|
||||
<source>MM</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.next" datatype="html">
|
||||
<source>»</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.datepicker.select-year" datatype="html">
|
||||
<source>Select year</source>
|
||||
<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-group>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.minutes" datatype="html">
|
||||
<source>Minutes</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.last" datatype="html">
|
||||
<source>»»</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.first-aria" datatype="html">
|
||||
<source>First</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
|
||||
<source>Increment hours</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
|
||||
<source>Previous</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
|
||||
<source>Decrement hours</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.next-aria" datatype="html">
|
||||
<source>Next</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
|
||||
<source>Increment minutes</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.last-aria" datatype="html">
|
||||
<source>Last</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
|
||||
<source>Decrement minutes</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.SS" datatype="html">
|
||||
<source>SS</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.seconds" datatype="html">
|
||||
<source>Seconds</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
|
||||
<source>Increment seconds</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
|
||||
<source>Decrement seconds</source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.PM" datatype="html">
|
||||
<source><x id="INTERPOLATION"/></source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
@@ -233,7 +233,7 @@
|
||||
<source><x id="INTERPOLATION" equiv-text="barConfig);
|
||||
pu"/></source>
|
||||
<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-group>
|
||||
</trans-unit>
|
||||
@@ -553,7 +553,7 @@
|
||||
</context-group>
|
||||
<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="linenumber">45</context>
|
||||
<context context-type="linenumber">55</context>
|
||||
</context-group>
|
||||
<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>
|
||||
@@ -1444,7 +1444,7 @@
|
||||
</context-group>
|
||||
<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="linenumber">44</context>
|
||||
<context context-type="linenumber">54</context>
|
||||
</context-group>
|
||||
<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>
|
||||
@@ -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>
|
||||
<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="linenumber">32</context>
|
||||
<context context-type="linenumber">42</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2739003406164860877" datatype="html">
|
||||
<source>Default Currency</source>
|
||||
<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="linenumber">37</context>
|
||||
<context context-type="linenumber">47</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7615210738790237590" datatype="html">
|
||||
<source>3-character currency code</source>
|
||||
<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="linenumber">37</context>
|
||||
<context context-type="linenumber">47</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="607636736207886379" datatype="html">
|
||||
<source>Use locale</source>
|
||||
<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="linenumber">37</context>
|
||||
<context context-type="linenumber">47</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="528950215505228201" datatype="html">
|
||||
<source>Create new custom field</source>
|
||||
<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="linenumber">93</context>
|
||||
<context context-type="linenumber">118</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8751213029607178010" datatype="html">
|
||||
<source>Edit custom field</source>
|
||||
<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="linenumber">97</context>
|
||||
<context context-type="linenumber">122</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6672809941092516947" datatype="html">
|
||||
|
@@ -11,27 +11,27 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^20.1.4",
|
||||
"@angular/common": "~20.1.4",
|
||||
"@angular/compiler": "~20.1.4",
|
||||
"@angular/core": "~20.1.4",
|
||||
"@angular/forms": "~20.1.4",
|
||||
"@angular/localize": "~20.1.4",
|
||||
"@angular/platform-browser": "~20.1.4",
|
||||
"@angular/platform-browser-dynamic": "~20.1.4",
|
||||
"@angular/router": "~20.1.4",
|
||||
"@angular/cdk": "^20.2.2",
|
||||
"@angular/common": "~20.2.4",
|
||||
"@angular/compiler": "~20.2.4",
|
||||
"@angular/core": "~20.2.4",
|
||||
"@angular/forms": "~20.2.4",
|
||||
"@angular/localize": "~20.2.4",
|
||||
"@angular/platform-browser": "~20.2.4",
|
||||
"@angular/platform-browser-dynamic": "~20.2.4",
|
||||
"@angular/router": "~20.2.4",
|
||||
"@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",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.7",
|
||||
"bootstrap": "^5.3.8",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^10.0.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
"ngx-device-detector": "^10.0.2",
|
||||
"ngx-cookie-service": "^20.1.0",
|
||||
"ngx-device-detector": "^10.1.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
@@ -42,20 +42,20 @@
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^20.0.0",
|
||||
"@angular-builders/jest": "^20.0.0",
|
||||
"@angular-devkit/core": "^20.1.4",
|
||||
"@angular-devkit/schematics": "^20.1.4",
|
||||
"@angular-eslint/builder": "20.1.1",
|
||||
"@angular-eslint/eslint-plugin": "20.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "20.1.1",
|
||||
"@angular-eslint/schematics": "20.1.1",
|
||||
"@angular-eslint/template-parser": "20.1.1",
|
||||
"@angular/build": "^20.1.4",
|
||||
"@angular/cli": "~20.1.4",
|
||||
"@angular/compiler-cli": "~20.1.4",
|
||||
"@angular-devkit/core": "^20.2.2",
|
||||
"@angular-devkit/schematics": "^20.2.2",
|
||||
"@angular-eslint/builder": "20.2.0",
|
||||
"@angular-eslint/eslint-plugin": "20.2.0",
|
||||
"@angular-eslint/eslint-plugin-template": "20.2.0",
|
||||
"@angular-eslint/schematics": "20.2.0",
|
||||
"@angular-eslint/template-parser": "20.2.0",
|
||||
"@angular/build": "^20.2.2",
|
||||
"@angular/cli": "~20.2.2",
|
||||
"@angular/compiler-cli": "~20.2.4",
|
||||
"@codecov/webpack-plugin": "^1.9.1",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@playwright/test": "^1.55.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/parser": "^8.38.0",
|
||||
"@typescript-eslint/utils": "^8.38.0",
|
||||
@@ -68,7 +68,7 @@
|
||||
"prettier-plugin-organize-imports": "^4.2.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.101.0"
|
||||
"webpack": "^5.101.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
2956
src-ui/pnpm-lock.yaml
generated
2956
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,16 @@
|
||||
</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) {
|
||||
<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>
|
||||
}
|
||||
|
@@ -125,4 +125,42 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
fixture.detectChanges()
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import {
|
||||
@@ -28,6 +29,8 @@ import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
|
||||
|
||||
const SELECT_OPTION_PAGE_SIZE = 8
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-custom-field-edit-dialog',
|
||||
templateUrl: './custom-field-edit-dialog.component.html',
|
||||
@@ -37,6 +40,7 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
|
||||
TextComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbPaginationModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
@@ -45,6 +49,21 @@ export class CustomFieldEditDialogComponent
|
||||
implements OnInit, AfterViewInit
|
||||
{
|
||||
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')
|
||||
private selectOptionInputs: QueryList<ElementRef>
|
||||
@@ -67,17 +86,10 @@ export class CustomFieldEditDialogComponent
|
||||
this.objectForm.get('data_type').disable()
|
||||
}
|
||||
if (this.object?.data_type === CustomFieldDataType.Select) {
|
||||
this.selectOptions.clear()
|
||||
this.object.extra_data.select_options
|
||||
.filter((option) => option)
|
||||
.forEach((option) =>
|
||||
this.selectOptions.push(
|
||||
new FormGroup({
|
||||
label: new FormControl(option.label),
|
||||
id: new FormControl(option.id),
|
||||
})
|
||||
)
|
||||
)
|
||||
this._allSelectOptions = [
|
||||
...(this.object.extra_data.select_options ?? []),
|
||||
]
|
||||
this.selectOptionsPage = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +99,19 @@ export class CustomFieldEditDialogComponent
|
||||
.subscribe(() => {
|
||||
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() {
|
||||
@@ -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() {
|
||||
return DATA_TYPE_LABELS
|
||||
}
|
||||
@@ -116,13 +152,35 @@ export class CustomFieldEditDialogComponent
|
||||
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() {
|
||||
this.selectOptions.push(
|
||||
new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
|
||||
this._allSelectOptions.push({ label: null, id: null })
|
||||
this.selectOptionsPage = Math.ceil(
|
||||
this.allSelectOptions.length / SELECT_OPTION_PAGE_SIZE
|
||||
)
|
||||
}
|
||||
|
||||
public removeSelectOption(index: number) {
|
||||
this.selectOptions.removeAt(index)
|
||||
this._allSelectOptions.splice(
|
||||
index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -147,9 +147,13 @@ export abstract class EditDialogComponent<
|
||||
)
|
||||
}
|
||||
|
||||
protected getFormValues(): any {
|
||||
return Object.assign({}, this.objectForm.value)
|
||||
}
|
||||
|
||||
save() {
|
||||
this.error = null
|
||||
const formValues = Object.assign({}, this.objectForm.value)
|
||||
const formValues = this.getFormValues()
|
||||
const permissionsObject: PermissionsFormObject =
|
||||
this.objectForm.get('permissions_form')?.value
|
||||
if (permissionsObject) {
|
||||
|
@@ -32,7 +32,7 @@ except ImportError: # pragma: no cover
|
||||
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
|
||||
and get or create Tag IDs for every directory.
|
||||
@@ -41,7 +41,7 @@ def _tags_from_path(filepath) -> list[int]:
|
||||
"""
|
||||
db.close_old_connections()
|
||||
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:
|
||||
tag_ids.add(
|
||||
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)
|
||||
|
||||
|
||||
def _is_ignored(filepath: str) -> bool:
|
||||
def _is_ignored(filepath: Path) -> bool:
|
||||
"""
|
||||
Checks if the given file should be ignored, based on configured
|
||||
patterns.
|
||||
|
||||
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
|
||||
# path relative to the consume directory
|
||||
filepath_relative = PurePath(filepath).relative_to(settings.CONSUMPTION_DIR)
|
||||
@@ -85,15 +81,15 @@ def _is_ignored(filepath: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _consume(filepath: str) -> None:
|
||||
if os.path.isdir(filepath) or _is_ignored(filepath):
|
||||
def _consume(filepath: Path) -> None:
|
||||
if filepath.is_dir() or _is_ignored(filepath):
|
||||
return
|
||||
|
||||
if not os.path.isfile(filepath):
|
||||
if not filepath.is_file():
|
||||
logger.debug(f"Not consuming file {filepath}: File has moved.")
|
||||
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.")
|
||||
return
|
||||
|
||||
@@ -107,7 +103,7 @@ def _consume(filepath: str) -> None:
|
||||
|
||||
while (read_try_count < os_error_retry_count) and not file_open_ok:
|
||||
try:
|
||||
with open(filepath, "rb"):
|
||||
with filepath.open("rb"):
|
||||
file_open_ok = True
|
||||
except OSError as e:
|
||||
read_try_count += 1
|
||||
@@ -141,7 +137,7 @@ def _consume(filepath: str) -> None:
|
||||
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
|
||||
and modification time. Will wait a configured number of seconds
|
||||
@@ -157,7 +153,7 @@ def _consume_wait_unmodified(file: str) -> None:
|
||||
current_try = 0
|
||||
while current_try < settings.CONSUMER_POLLING_RETRY_COUNT:
|
||||
try:
|
||||
stat_data = os.stat(file)
|
||||
stat_data = file.stat()
|
||||
new_mtime = stat_data.st_mtime
|
||||
new_size = stat_data.st_size
|
||||
except FileNotFoundError:
|
||||
@@ -182,10 +178,10 @@ class Handler(FileSystemEventHandler):
|
||||
self._pool = pool
|
||||
|
||||
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):
|
||||
self._pool.submit(_consume_wait_unmodified, event.dest_path)
|
||||
self._pool.submit(_consume_wait_unmodified, Path(event.dest_path))
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -227,9 +223,9 @@ class Command(BaseCommand):
|
||||
if not directory:
|
||||
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")
|
||||
|
||||
# Consumer will need this
|
||||
@@ -238,11 +234,11 @@ class Command(BaseCommand):
|
||||
if recursive:
|
||||
for dirpath, _, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
filepath = Path(dirpath) / filename
|
||||
_consume(filepath)
|
||||
else:
|
||||
for entry in os.scandir(directory):
|
||||
_consume(entry.path)
|
||||
for filepath in directory.iterdir():
|
||||
_consume(filepath)
|
||||
|
||||
if options["oneshot"]:
|
||||
return
|
||||
@@ -310,7 +306,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
for event in inotify.read(timeout=timeout_ms):
|
||||
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):
|
||||
notified_files.pop(filepath, None)
|
||||
else:
|
||||
@@ -327,9 +323,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Also make sure the file exists still, some scanners might write a
|
||||
# temporary file first
|
||||
file_still_exists = os.path.exists(filepath) and os.path.isfile(
|
||||
filepath,
|
||||
)
|
||||
file_still_exists = filepath.exists() and filepath.is_file()
|
||||
|
||||
if waited_long_enough and file_still_exists:
|
||||
_consume(filepath)
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
import pathvalidate
|
||||
@@ -50,38 +51,38 @@ def many_to_dictionary(field): # pragma: no cover
|
||||
return mydictionary
|
||||
|
||||
|
||||
def archive_name_from_filename(filename):
|
||||
return os.path.splitext(filename)[0] + ".pdf"
|
||||
def archive_name_from_filename(filename: Path) -> Path:
|
||||
return Path(filename.stem + ".pdf")
|
||||
|
||||
|
||||
def archive_path_old(doc):
|
||||
def archive_path_old(doc) -> Path:
|
||||
if doc.filename:
|
||||
fname = archive_name_from_filename(doc.filename)
|
||||
fname = archive_name_from_filename(Path(doc.filename))
|
||||
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"
|
||||
|
||||
|
||||
def archive_path_new(doc):
|
||||
def archive_path_new(doc) -> Path | 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:
|
||||
return None
|
||||
|
||||
|
||||
def source_path(doc):
|
||||
def source_path(doc) -> Path:
|
||||
if doc.filename:
|
||||
fname = str(doc.filename)
|
||||
fname = doc.filename
|
||||
else:
|
||||
fname = f"{doc.pk:07}{doc.file_type}"
|
||||
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):
|
||||
@@ -104,7 +105,7 @@ def generate_unique_filename(doc, *, archive_filename=False):
|
||||
# still the same as before.
|
||||
return new_filename
|
||||
|
||||
if os.path.exists(os.path.join(root, new_filename)):
|
||||
if (root / new_filename).exists():
|
||||
counter += 1
|
||||
else:
|
||||
return new_filename
|
||||
@@ -202,18 +203,18 @@ def create_archive_version(doc, retry_count=3):
|
||||
parser,
|
||||
source_path(doc),
|
||||
doc.mime_type,
|
||||
os.path.basename(doc.filename),
|
||||
Path(doc.filename).name,
|
||||
)
|
||||
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=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()
|
||||
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))
|
||||
else:
|
||||
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
|
||||
for doc in Document.objects.filter(archive_checksum__isnull=False):
|
||||
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(
|
||||
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:
|
||||
old_path = archive_path_old(doc)
|
||||
# remove affected archive versions
|
||||
if os.path.isfile(old_path):
|
||||
if old_path.is_file():
|
||||
logger.debug(f"Removing {old_path}")
|
||||
os.unlink(old_path)
|
||||
old_path.unlink()
|
||||
else:
|
||||
# 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(
|
||||
archive_filename=doc.archive_filename,
|
||||
)
|
||||
@@ -316,7 +317,7 @@ def move_new_to_old_locations(apps, schema_editor):
|
||||
f"filename.",
|
||||
)
|
||||
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(
|
||||
f"Cannot migrate: Cannot move {new_archive_path} to "
|
||||
f"{old_archive_path}: file already exists.",
|
||||
|
@@ -169,7 +169,7 @@ def run_convert(
|
||||
args += ["-depth", str(depth)] if depth else []
|
||||
args += ["-auto-orient"] if auto_orient 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})
|
||||
|
||||
@@ -188,8 +188,8 @@ def get_default_thumbnail() -> Path:
|
||||
return (Path(__file__).parent / "resources" / "document.webp").resolve()
|
||||
|
||||
|
||||
def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None) -> str:
|
||||
out_path = os.path.join(temp_dir, "convert_gs.webp")
|
||||
def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None) -> Path:
|
||||
out_path: Path = Path(temp_dir) / "convert_gs.webp"
|
||||
|
||||
# if convert fails, fall back to extracting
|
||||
# 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},
|
||||
)
|
||||
# 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]
|
||||
|
||||
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,
|
||||
# so we need to copy it before it gets moved.
|
||||
# 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)
|
||||
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.
|
||||
"""
|
||||
out_path = temp_dir / "convert.webp"
|
||||
out_path: Path = temp_dir / "convert.webp"
|
||||
|
||||
# Run convert to get a decent thumbnail
|
||||
try:
|
||||
|
6
src/documents/static/bootstrap.min.css
vendored
6
src/documents/static/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -654,7 +654,7 @@ class TestClassifier(DirectoriesMixin, TestCase):
|
||||
},
|
||||
)
|
||||
@override_settings(
|
||||
MODEL_FILE=(Path(__file__).parent / "data" / "model.pickle").as_posix(),
|
||||
MODEL_FILE=str(Path(__file__).parent / "data" / "model.pickle"),
|
||||
)
|
||||
@pytest.mark.skip(
|
||||
reason="Disabled caching due to high memory usage - need to investigate.",
|
||||
|
@@ -254,7 +254,7 @@ class TestConsumer(
|
||||
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
|
||||
|
||||
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)
|
||||
|
||||
|
@@ -258,66 +258,66 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
|
||||
def test_is_ignored(self):
|
||||
test_paths = [
|
||||
{
|
||||
"path": (Path(self.dirs.consumption_dir) / "foo.pdf").as_posix(),
|
||||
"path": str(Path(self.dirs.consumption_dir) / "foo.pdf"),
|
||||
"ignore": False,
|
||||
},
|
||||
{
|
||||
"path": (
|
||||
Path(self.dirs.consumption_dir) / "foo" / "bar.pdf"
|
||||
).as_posix(),
|
||||
"path": str(
|
||||
Path(self.dirs.consumption_dir) / "foo" / "bar.pdf",
|
||||
),
|
||||
"ignore": False,
|
||||
},
|
||||
{
|
||||
"path": (Path(self.dirs.consumption_dir) / ".DS_STORE").as_posix(),
|
||||
"path": str(Path(self.dirs.consumption_dir) / ".DS_STORE"),
|
||||
"ignore": True,
|
||||
},
|
||||
{
|
||||
"path": (Path(self.dirs.consumption_dir) / ".DS_Store").as_posix(),
|
||||
"path": str(Path(self.dirs.consumption_dir) / ".DS_Store"),
|
||||
"ignore": True,
|
||||
},
|
||||
{
|
||||
"path": (
|
||||
Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf"
|
||||
).as_posix(),
|
||||
"path": str(
|
||||
Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf",
|
||||
),
|
||||
"ignore": True,
|
||||
},
|
||||
{
|
||||
"path": (Path(self.dirs.consumption_dir) / ".stfolder.pdf").as_posix(),
|
||||
"path": str(Path(self.dirs.consumption_dir) / ".stfolder.pdf"),
|
||||
"ignore": False,
|
||||
},
|
||||
{
|
||||
"path": (
|
||||
Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf"
|
||||
).as_posix(),
|
||||
"path": str(
|
||||
Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf",
|
||||
),
|
||||
"ignore": True,
|
||||
},
|
||||
{
|
||||
"path": (
|
||||
Path(self.dirs.consumption_dir) / ".stversions.pdf"
|
||||
).as_posix(),
|
||||
"path": str(
|
||||
Path(self.dirs.consumption_dir) / ".stversions.pdf",
|
||||
),
|
||||
"ignore": False,
|
||||
},
|
||||
{
|
||||
"path": (Path(self.dirs.consumption_dir) / "._foo.pdf").as_posix(),
|
||||
"path": str(Path(self.dirs.consumption_dir) / "._foo.pdf"),
|
||||
"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,
|
||||
},
|
||||
{
|
||||
"path": (
|
||||
Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf"
|
||||
).as_posix(),
|
||||
"path": str(
|
||||
Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf",
|
||||
),
|
||||
"ignore": True,
|
||||
},
|
||||
{
|
||||
"path": (
|
||||
"path": str(
|
||||
Path(self.dirs.consumption_dir)
|
||||
/ "@eaDir"
|
||||
/ "SYNO@.fileindexdb"
|
||||
/ "_1jk.fnm"
|
||||
).as_posix(),
|
||||
/ "_1jk.fnm",
|
||||
),
|
||||
"ignore": True,
|
||||
},
|
||||
]
|
||||
@@ -330,7 +330,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
|
||||
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):
|
||||
# Calling this mock always raises this
|
||||
open_mock.side_effect = OSError
|
||||
|
@@ -230,9 +230,9 @@ class TestExportImport(
|
||||
|
||||
for element in manifest:
|
||||
if element["model"] == "documents.document":
|
||||
fname = (
|
||||
self.target / element[document_exporter.EXPORTER_FILE_NAME]
|
||||
).as_posix()
|
||||
fname = str(
|
||||
self.target / element[document_exporter.EXPORTER_FILE_NAME],
|
||||
)
|
||||
self.assertIsFile(fname)
|
||||
self.assertIsFile(
|
||||
self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME],
|
||||
@@ -462,9 +462,9 @@ class TestExportImport(
|
||||
|
||||
call_command(*args)
|
||||
|
||||
expected_file = (
|
||||
self.target / f"export-{timezone.localdate().isoformat()}.zip"
|
||||
).as_posix()
|
||||
expected_file = str(
|
||||
self.target / f"export-{timezone.localdate().isoformat()}.zip",
|
||||
)
|
||||
|
||||
self.assertIsFile(expected_file)
|
||||
|
||||
@@ -498,9 +498,9 @@ class TestExportImport(
|
||||
):
|
||||
call_command(*args)
|
||||
|
||||
expected_file = (
|
||||
self.target / f"export-{timezone.localdate().isoformat()}.zip"
|
||||
).as_posix()
|
||||
expected_file = str(
|
||||
self.target / f"export-{timezone.localdate().isoformat()}.zip",
|
||||
)
|
||||
|
||||
self.assertIsFile(expected_file)
|
||||
|
||||
@@ -544,9 +544,9 @@ class TestExportImport(
|
||||
|
||||
call_command(*args)
|
||||
|
||||
expected_file = (
|
||||
self.target / f"export-{timezone.localdate().isoformat()}.zip"
|
||||
).as_posix()
|
||||
expected_file = str(
|
||||
self.target / f"export-{timezone.localdate().isoformat()}.zip",
|
||||
)
|
||||
|
||||
self.assertIsFile(expected_file)
|
||||
self.assertIsNotFile(existing_file)
|
||||
|
@@ -19,15 +19,15 @@ migration_1012_obj = importlib.import_module(
|
||||
)
|
||||
|
||||
|
||||
def archive_name_from_filename(filename):
|
||||
return Path(filename).stem + ".pdf"
|
||||
def archive_name_from_filename(filename: Path) -> Path:
|
||||
return Path(filename.stem + ".pdf")
|
||||
|
||||
|
||||
def archive_path_old(self):
|
||||
def archive_path_old(self) -> Path:
|
||||
if self.filename:
|
||||
fname = archive_name_from_filename(self.filename)
|
||||
fname = archive_name_from_filename(Path(self.filename))
|
||||
else:
|
||||
fname = f"{self.pk:07}.pdf"
|
||||
fname = Path(f"{self.pk:07}.pdf")
|
||||
|
||||
return Path(settings.ARCHIVE_DIR) / fname
|
||||
|
||||
|
@@ -679,7 +679,7 @@ def _parse_db_settings() -> dict:
|
||||
databases = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": str(DATA_DIR / "db.sqlite3"),
|
||||
"NAME": DATA_DIR / "db.sqlite3",
|
||||
"OPTIONS": {},
|
||||
},
|
||||
}
|
||||
@@ -807,7 +807,7 @@ LANGUAGES = [
|
||||
("zh-tw", _("Chinese Traditional")),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [str(BASE_DIR / "locale")]
|
||||
LOCALE_PATHS = [BASE_DIR / "locale"]
|
||||
|
||||
TIME_ZONE = os.getenv("PAPERLESS_TIME_ZONE", "UTC")
|
||||
|
||||
@@ -848,21 +848,21 @@ LOGGING = {
|
||||
"file_paperless": {
|
||||
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
|
||||
"formatter": "verbose",
|
||||
"filename": str(LOGGING_DIR / "paperless.log"),
|
||||
"filename": LOGGING_DIR / "paperless.log",
|
||||
"maxBytes": LOGROTATE_MAX_SIZE,
|
||||
"backupCount": LOGROTATE_MAX_BACKUPS,
|
||||
},
|
||||
"file_mail": {
|
||||
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
|
||||
"formatter": "verbose",
|
||||
"filename": str(LOGGING_DIR / "mail.log"),
|
||||
"filename": LOGGING_DIR / "mail.log",
|
||||
"maxBytes": LOGROTATE_MAX_SIZE,
|
||||
"backupCount": LOGROTATE_MAX_BACKUPS,
|
||||
},
|
||||
"file_celery": {
|
||||
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
|
||||
"formatter": "verbose",
|
||||
"filename": str(LOGGING_DIR / "celery.log"),
|
||||
"filename": LOGGING_DIR / "celery.log",
|
||||
"maxBytes": LOGROTATE_MAX_SIZE,
|
||||
"backupCount": LOGROTATE_MAX_BACKUPS,
|
||||
},
|
||||
@@ -921,7 +921,7 @@ CELERY_ACCEPT_CONTENT = ["application/json", "application/x-python-serialize"]
|
||||
CELERY_BEAT_SCHEDULE = _parse_beat_schedule()
|
||||
|
||||
# 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.
|
||||
|
@@ -69,13 +69,13 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(uuid.uuid4())
|
||||
page_count = parser.get_page_count(
|
||||
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "simple-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertEqual(page_count, 1)
|
||||
|
||||
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",
|
||||
)
|
||||
self.assertEqual(page_count, 6)
|
||||
@@ -92,7 +92,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(uuid.uuid4())
|
||||
with self.assertLogs("paperless.parsing.tesseract", level="WARNING") as cm:
|
||||
page_count = parser.get_page_count(
|
||||
(self.SAMPLE_FILES / "password-protected.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "password-protected.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertEqual(page_count, None)
|
||||
@@ -101,7 +101,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_thumbnail(self):
|
||||
parser = RasterisedDocumentParser(uuid.uuid4())
|
||||
thumb = parser.get_thumbnail(
|
||||
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "simple-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(thumb)
|
||||
@@ -109,7 +109,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
@mock.patch("documents.parsers.run_convert")
|
||||
def test_thumbnail_fallback(self, m):
|
||||
def call_convert(input_file, output_file, **kwargs):
|
||||
if ".pdf" in input_file:
|
||||
if ".pdf" in str(input_file):
|
||||
raise ParseError("Does not compute.")
|
||||
else:
|
||||
run_convert(input_file=input_file, output_file=output_file, **kwargs)
|
||||
@@ -118,7 +118,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
|
||||
parser = RasterisedDocumentParser(uuid.uuid4())
|
||||
thumb = parser.get_thumbnail(
|
||||
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "simple-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(thumb)
|
||||
@@ -126,7 +126,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_thumbnail_encrypted(self):
|
||||
parser = RasterisedDocumentParser(uuid.uuid4())
|
||||
thumb = parser.get_thumbnail(
|
||||
(self.SAMPLE_FILES / "encrypted.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "encrypted.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(thumb)
|
||||
@@ -134,17 +134,17 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_get_dpi(self):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def test_simple_digital(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "simple-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -156,7 +156,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "with-form.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "with-form.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -172,7 +172,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "with-form.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "with-form.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -186,7 +186,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_signed(self):
|
||||
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.assertContainsStrings(
|
||||
@@ -202,7 +202,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "encrypted.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "encrypted.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_with_form_error_notext(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "with-form.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "with-form.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -227,7 +227,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "with-form.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "with-form.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -239,7 +239,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_image_simple(self):
|
||||
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)
|
||||
|
||||
@@ -255,7 +255,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
dest_file = Path(tempdir) / "simple-alpha.png"
|
||||
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)
|
||||
|
||||
@@ -265,7 +265,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
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)
|
||||
@@ -277,7 +277,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
|
||||
def f():
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "simple-no-dpi.png").as_posix(),
|
||||
str(self.SAMPLE_FILES / "simple-no-dpi.png"),
|
||||
"image/png",
|
||||
)
|
||||
|
||||
@@ -287,7 +287,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_image_no_dpi_default(self):
|
||||
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)
|
||||
|
||||
@@ -299,7 +299,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_multi_page(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -312,7 +312,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_multi_page_pages_skip(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -325,7 +325,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_multi_page_pages_redo(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -338,7 +338,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_multi_page_pages_force(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -351,7 +351,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_multi_page_analog_pages_skip(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -375,7 +375,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -397,7 +397,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -419,7 +419,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNone(parser.archive_path)
|
||||
@@ -442,7 +442,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -467,7 +467,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNotNone(parser.archive_path)
|
||||
@@ -490,7 +490,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNotNone(parser.archive_path)
|
||||
@@ -513,7 +513,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNone(parser.archive_path)
|
||||
@@ -536,7 +536,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNotNone(parser.archive_path)
|
||||
@@ -559,7 +559,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNone(parser.archive_path)
|
||||
@@ -582,7 +582,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNone(parser.archive_path)
|
||||
@@ -605,7 +605,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-mixed.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-mixed.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNotNone(parser.archive_path)
|
||||
@@ -636,7 +636,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "single-page-mixed.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "single-page-mixed.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNotNone(parser.archive_path)
|
||||
@@ -673,7 +673,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-mixed.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-mixed.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
self.assertIsNone(parser.archive_path)
|
||||
@@ -685,7 +685,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
@override_settings(OCR_MODE="skip", OCR_ROTATE_PAGES=True)
|
||||
def test_rotate(self):
|
||||
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(
|
||||
parser.get_text(),
|
||||
[
|
||||
@@ -707,7 +707,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "multi-page-images.tiff").as_posix(),
|
||||
str(self.SAMPLE_FILES / "multi-page-images.tiff"),
|
||||
"image/tiff",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
@@ -752,9 +752,9 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
- Text from all pages extracted
|
||||
"""
|
||||
parser = RasterisedDocumentParser(None)
|
||||
sample_file = (
|
||||
self.SAMPLE_FILES / "multi-page-images-alpha-rgb.tiff"
|
||||
).as_posix()
|
||||
sample_file = str(
|
||||
self.SAMPLE_FILES / "multi-page-images-alpha-rgb.tiff",
|
||||
)
|
||||
with tempfile.NamedTemporaryFile() as tmp_file:
|
||||
shutil.copy(sample_file, tmp_file.name)
|
||||
parser.parse(
|
||||
@@ -843,7 +843,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "rtl-test.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "rtl-test.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -858,7 +858,7 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
self.assertRaises(
|
||||
ParseError,
|
||||
parser.parse,
|
||||
(self.SAMPLE_FILES / "simple-digital.pdf").as_posix(),
|
||||
str(self.SAMPLE_FILES / "simple-digital.pdf"),
|
||||
"application/pdf",
|
||||
)
|
||||
|
||||
@@ -868,32 +868,32 @@ class TestParserFileTypes(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
|
||||
def test_bmp(self):
|
||||
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.assertIn("this is a test document", parser.get_text().lower())
|
||||
|
||||
def test_jpg(self):
|
||||
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.assertIn("this is a test document", parser.get_text().lower())
|
||||
|
||||
def test_heic(self):
|
||||
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.assertIn("pizza", parser.get_text().lower())
|
||||
|
||||
@override_settings(OCR_IMAGE_DPI=200)
|
||||
def test_gif(self):
|
||||
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.assertIn("this is a test document", parser.get_text().lower())
|
||||
|
||||
def test_tiff(self):
|
||||
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.assertIn("this is a test document", parser.get_text().lower())
|
||||
|
||||
@@ -901,7 +901,7 @@ class TestParserFileTypes(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
def test_webp(self):
|
||||
parser = RasterisedDocumentParser(None)
|
||||
parser.parse(
|
||||
(self.SAMPLE_FILES / "document.webp").as_posix(),
|
||||
str(self.SAMPLE_FILES / "document.webp"),
|
||||
"image/webp",
|
||||
)
|
||||
self.assertIsFile(parser.archive_path)
|
||||
|
Reference in New Issue
Block a user