mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			b9e34bd793
			...
			release-wo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3cfb0f5856 | ||
|   | a79c8dc51c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4b95c2f0e5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e1c8cd779b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cc7c7f31ba | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1d30ce2afa | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5aa86f8755 | ||
|   | de2ddad5ee | ||
|   | d2064a2535 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cc621cf729 | ||
|   | fc4134e15c | ||
|   | ac1b420966 | ||
|   | 80595899c1 | 
| @@ -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" | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										8
									
								
								src/documents/static/bootstrap.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								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