Compare commits

...
Author SHA1 Message Date
Crowdin Bot cfd5869f82 New Crowdin translations by GitHub Action 2026-08-08 14:30:06 +00:00
GitHub Actions b0e0e8a353 Auto translate strings 2026-08-08 14:29:01 +00:00
fc242bb570 Performance: unify permission-filtering backends, fixes Correspondent/Tag list slowness (#13601)
* feat: add unified PermittedObjectsFilter backed by permitted_object_ids

* refactor: migrate all ViewSets to unified PermittedObjectsFilter

Replace the deprecated ObjectOwnedOrGrantedPermissionsFilter,
DocumentPermissionsFilter, and ObjectOwnedPermissionsFilter aliases
with PermittedObjectsFilter directly across documents/views.py (8
sites, including TrashView's include_granted=False subclass) and
paperless_mail/views.py (3 sites), then delete the now-unreferenced
alias classes from documents/filters.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFyrt7FWbRRdTUAcdqBcsc

* docs: document legacy status of get_objects_for_user_owner_aware/has_perms_owner_aware

Stage 4's PermittedObjectsFilter/permitted_object_ids() covers the
queryset-filtering use case, but both functions still have production
callers outside this plan's scope (documents/views.py,
documents/serialisers.py, documents/signals/handlers.py,
paperless_ai/matching.py, paperless_ai/ai_classifier.py). Per Task 20
Step 2, they are kept in place rather than partially deleted, with
docstrings updated to note their legacy status and remaining callers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFyrt7FWbRRdTUAcdqBcsc

* Fix: address final review findings for permission-filter unification

- Add a permanent regression test pinning TrashView's include_granted=False
  wiring: an explicit view_document grant on a trashed document must not
  leak it into /api/trash/ for a non-owner, non-superuser requester.
- Drop the now-dead direct dependency djangorestframework-guardian; the
  last rest_framework_guardian import was removed by this branch's
  migration onto PermittedObjectsFilter. django-guardian is untouched.
- Replace the hand-maintained, already-stale caller lists in
  get_objects_for_user_owner_aware/has_perms_owner_aware docstrings with a
  pointer to grep for remaining callers instead.
- In PermittedObjectsFilter.filter_queryset, compute `model` only on the
  include_granted=True path that actually uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFyrt7FWbRRdTUAcdqBcsc

* perf: check bulk-edit-objects apply_to_all permissions via DB-side exclude/exists

Materialized the full permitted_object_ids() set into a Python set() just
to check membership for the request's objs queryset -- the same pattern
already fixed at four other sites for Document. This one is used by
apply_to_all, where objs can be an unbounded filtered selection (e.g. all
tags matching a filter) rather than a small request-supplied ID list,
making the wasted materialization worse here than at the sites already
fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Cleans up the comment about why this is still here for now

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 07:27:18 -07:00
b192a419fd perf: migrate bulk-edit-objects dispatch to permitted_object_ids (#13576)
* perf: migrate bulk-edit-objects apply_to_all dispatch to permitted_object_ids

Replaces get_objects_for_user_owner_aware/has_perms_owner_aware in the
BulkEditObjectsView apply_to_all dispatch (Tag/Correspondent/DocumentType/
StoragePath) with permitted_object_ids and the resolve-once,
check-membership pattern used elsewhere in this stage. Tag-descendant
expansion logic left untouched. Adds a security test pinning that
apply_to_all excludes objects the requester lacks object-level permission
on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmMBGW9FKyDgmKRJ5H9rif

* test: add tag-descendant partial-permission coverage, verify pre-migration characterization

Adds TestBulkEditObjectsTagDescendantPartialPermission, exercising the
tag-descendant-expansion block in BulkEditObjectsView.post as a
non-superuser with object-level change_tag granted on a parent tag and
one of two children but not the other, confirming the expansion only
pulls in descendants the requester actually has permission on.

Verified both this test and the existing apply_to_all boundary test
pass unchanged against the pre-migration
get_objects_for_user_owner_aware/has_perms_owner_aware code (reverted
via a scratch patch of the prior commit's views.py hunk, then
restored), confirming they characterize genuine pre-existing behavior
rather than something the permitted_object_ids migration made
necessary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmMBGW9FKyDgmKRJ5H9rif

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 07:27:17 -07:00
3986150f95 perf: migrate matching.py's classification lookups to permitted_object_ids (#13575)
* perf: migrate matching.py's 4 permission-filtered lookups to permitted_object_ids

* test: add matching.py permission coverage for correspondents, document types, storage paths

Completes the parametrized coverage started for tags -- proves all 4
matching.py lookups migrated to permitted_object_ids respect
per-object view permissions, not just the tag case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 07:27:17 -07:00
ee5588ade3 Performance: generalize permitted_document_ids into permitted_object_ids for any model (#13578)
* feat: generalize permitted_document_ids into permitted_object_ids for any model

Implements Task 14 of the permission-filtering consolidation plan:
- Add generic permitted_object_ids(user, model, perm, include_deleted=False)
- Refactor permitted_document_ids to delegate to permitted_object_ids
- Add comprehensive tests for Tag/Correspondent/DocumentType/StoragePath
- Preserve exact public behavior of permitted_document_ids (100% regression-free)

All 38 tests pass (18 existing + 20 new). The include_deleted parameter
correctly handles soft-delete patterns (effective only for Document).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor: add type hints to permitted_object_ids and permitted_document_ids

Add missing type annotations to match the established conventions in this file
(see get_objects_for_user_owner_aware). Also added Model import from django.db.models.

- permitted_object_ids: user: User | None, model: type[Model], return -> QuerySet[int]
- permitted_document_ids: user: User | None, return -> QuerySet[int]

All 38 permission filtering security tests pass; this is a type-annotation-only change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmMBGW9FKyDgmKRJ5H9rif

* refactor: remove redundant deleted_at filter in permitted_object_ids

SoftDeleteManager's own get_queryset() already excludes soft-deleted
rows, so the extra deleted_at__isnull=True filter was dead code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 07:27:16 -07:00
shamoonandGitHub 2635a12281 Fix: render PDF form values in annotation layer (#13607) 2026-08-07 23:15:42 -07:00
shamoonandGitHub 1f396e51f3 QoL: disable name button without perms (#13606) 2026-08-07 23:01:24 -07:00
GitHub Actions 3eb784b34d Auto translate strings 2026-08-07 20:01:16 +00:00
shamoonandGitHub cb03a0b33e Fix: fix broken docker frontend from esbuild migration (#13603) 2026-08-07 12:59:46 -07:00
GitHub Actions a96d0b15b8 Auto translate strings 2026-08-07 18:53:58 +00:00
shamoonandGitHub 376b61938f Fix: prevent debounce overwrites in advanced search field, also improve Esc behavior (#13602) 2026-08-07 11:52:24 -07:00
Trenton HandGitHub d1f5eb0335 Performance: reduce memory and I/O overhead of the document exporter during zip exports (#13490) 2026-08-07 18:50:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d283205f57 Chore(deps): Bump h2 in the uv group across 1 directory (#13593)
Bumps the uv group with 1 update in the / directory: [h2](https://github.com/python-hyper/h2).


Updates `h2` from 4.3.0 to 4.4.1
- [Changelog](https://github.com/python-hyper/h2/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/python-hyper/h2/compare/v4.3.0...v4.4.1)

---
updated-dependencies:
- dependency-name: h2
  dependency-version: 4.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 16:54:38 +00:00
shamoonandGitHub 42908ad2b9 Chore: remove codecov/webpack-plugin (#13595) 2026-08-06 23:48:07 -07:00
GitHub Actions db6842e710 Auto translate strings 2026-08-07 05:38:12 +00:00
shamoonandGitHub d5f8cd59fb Chore: migrate frontend to esbuild from webpack (#13488) 2026-08-06 22:36:17 -07:00
dependabot[bot]andGitHub 91d6741b4f Chore(deps): Bump pdfjs-dist from 6.1.200 to 6.2.108 in /src-ui in the npm_and_yarn group across 1 directory (#13594)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-07 04:40:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
972758fc72 Chore(deps): Bump cryptography in the uv group across 1 directory (#13588)
Bumps the uv group with 1 update in the / directory: [cryptography](https://github.com/pyca/cryptography).


Updates `cryptography` from 48.0.1 to 50.0.0
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/48.0.1...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 21:12:13 -07:00
shamoonandGitHub 15d9829b6d QoL: make name button text on attribute pages selectable (#13592) 2026-08-06 18:56:04 -07:00
Trenton HandGitHub 5ad34dfe03 Fix: Set the document modified dates to hopefully prevent this flake. Good across 30 runs at least (#13584) 2026-08-07 01:06:02 +00:00
126 changed files with 44108 additions and 43236 deletions
+14 -19
View File
@@ -129,8 +129,8 @@ jobs:
~/.pnpm-store ~/.pnpm-store
~/.cache ~/.cache
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }} key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular CLI - name: Install dependencies
run: cd src-ui && pnpm link @angular/cli run: cd src-ui && pnpm install --frozen-lockfile
- name: Run lint - name: Run lint
run: cd src-ui && pnpm run lint run: cd src-ui && pnpm run lint
unit-tests: unit-tests:
@@ -168,8 +168,8 @@ jobs:
~/.pnpm-store ~/.pnpm-store
~/.cache ~/.cache
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }} key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular CLI - name: Install dependencies
run: cd src-ui && pnpm link @angular/cli run: cd src-ui && pnpm install --frozen-lockfile
- name: Run Jest unit tests - name: Run Jest unit tests
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload test results to Codecov - name: Upload test results to Codecov
@@ -223,18 +223,15 @@ jobs:
~/.pnpm-store ~/.pnpm-store
~/.cache ~/.cache
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }} key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular CLI
run: cd src-ui && pnpm link @angular/cli
- name: Install dependencies - name: Install dependencies
run: cd src-ui && pnpm install --no-frozen-lockfile run: cd src-ui && pnpm install --frozen-lockfile
- name: Run Playwright E2E tests - name: Run Playwright E2E tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
bundle-analysis: frontend-build:
name: Bundle Analysis name: Frontend Build
needs: [changes, unit-tests, e2e-tests] needs: [changes, unit-tests, e2e-tests]
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: bundle-analysis
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -260,21 +257,19 @@ jobs:
~/.pnpm-store ~/.pnpm-store
~/.cache ~/.cache
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }} key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular CLI - name: Install dependencies
run: cd src-ui && pnpm link @angular/cli run: cd src-ui && pnpm install --frozen-lockfile
- name: Build and analyze - name: Build
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: cd src-ui && pnpm run build --configuration=production run: cd src-ui && pnpm run build --configuration=production
gate: gate:
name: Frontend CI Gate name: Frontend CI Gate
needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, bundle-analysis] needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, frontend-build]
if: always() if: always()
runs-on: ubuntu-slim runs-on: ubuntu-slim
steps: steps:
- name: Check gate - name: Check gate
env: env:
BUNDLE_ANALYSIS_RESULT: ${{ needs['bundle-analysis'].result }} BUILD_RESULT: ${{ needs['frontend-build'].result }}
E2E_RESULT: ${{ needs['e2e-tests'].result }} E2E_RESULT: ${{ needs['e2e-tests'].result }}
FRONTEND_CHANGED: ${{ needs.changes.outputs.frontend_changed }} FRONTEND_CHANGED: ${{ needs.changes.outputs.frontend_changed }}
INSTALL_RESULT: ${{ needs['install-dependencies'].result }} INSTALL_RESULT: ${{ needs['install-dependencies'].result }}
@@ -306,8 +301,8 @@ jobs:
exit 1 exit 1
fi fi
if [[ "${BUNDLE_ANALYSIS_RESULT}" != "success" ]]; then if [[ "${BUILD_RESULT}" != "success" ]]; then
echo "::error::Frontend bundle-analysis job result: ${BUNDLE_ANALYSIS_RESULT}" echo "::error::Frontend build job result: ${BUILD_RESULT}"
exit 1 exit 1
fi fi
+1 -4
View File
@@ -61,10 +61,7 @@ jobs:
~/.cache ~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }} key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Install frontend dependencies - name: Install frontend dependencies
if: steps.cache-frontend-deps.outputs.cache-hit != 'true' run: cd src-ui && pnpm install --frozen-lockfile
run: cd src-ui && pnpm install
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Generate frontend translation strings - name: Generate frontend translation strings
run: | run: |
cd src-ui cd src-ui
-1
View File
@@ -38,7 +38,6 @@ dependencies = [
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
"django-treenode>=0.24", "django-treenode>=0.24",
"djangorestframework~=3.16", "djangorestframework~=3.16",
"djangorestframework-guardian~=0.4.0",
"drf-spectacular~=0.30", "drf-spectacular~=0.30",
"drf-spectacular-sidecar~=2026.7.1", "drf-spectacular-sidecar~=2026.7.1",
"drf-writable-nested~=0.7.1", "drf-writable-nested~=0.7.1",
+12 -9
View File
@@ -56,13 +56,13 @@
}, },
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-builders/custom-webpack:browser", "builder": "@angular/build:application",
"options": { "options": {
"customWebpackConfig": { "outputPath": {
"path": "./extra-webpack.config.ts" "base": "dist/paperless-ui",
"browser": ""
}, },
"outputPath": "dist/paperless-ui", "browser": "src/main.ts",
"main": "src/main.ts",
"outputHashing": "none", "outputHashing": "none",
"index": "src/index.html", "index": "src/index.html",
"polyfills": [ "polyfills": [
@@ -97,6 +97,7 @@
"scripts": [], "scripts": [],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"file-saver", "file-saver",
"mime-names",
"utif" "utif"
], ],
"extractLicenses": false, "extractLicenses": false,
@@ -117,11 +118,13 @@
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
} }
], ],
"outputPath": "../src/documents/static/frontend/", "outputPath": {
"base": "../src/documents/static/frontend/",
"browser": ""
},
"optimization": true, "optimization": true,
"outputHashing": "none", "outputHashing": "none",
"sourceMap": false, "sourceMap": false,
"namedChunks": false,
"extractLicenses": true, "extractLicenses": true,
"budgets": [ "budgets": [
{ {
@@ -145,7 +148,7 @@
"defaultConfiguration": "" "defaultConfiguration": ""
}, },
"serve": { "serve": {
"builder": "@angular-builders/custom-webpack:dev-server", "builder": "@angular/build:dev-server",
"options": { "options": {
"buildTarget": "paperless-ui:build:en-US" "buildTarget": "paperless-ui:build:en-US"
}, },
@@ -156,7 +159,7 @@
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-builders/custom-webpack:extract-i18n", "builder": "@angular/build:extract-i18n",
"options": { "options": {
"buildTarget": "paperless-ui:build" "buildTarget": "paperless-ui:build"
} }
-24
View File
@@ -1,24 +0,0 @@
import {
CustomWebpackBrowserSchema,
TargetOptions,
} from '@angular-builders/custom-webpack'
import * as webpack from 'webpack'
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
export default (
config: webpack.Configuration,
options: CustomWebpackBrowserSchema,
targetOptions: TargetOptions
) => {
if (config.plugins) {
config.plugins.push(
codecovWebpackPlugin({
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
bundleName: 'paperless-ngx',
uploadToken: process.env.CODECOV_TOKEN,
})
)
}
return config
}
+708 -709
View File
File diff suppressed because it is too large Load Diff
+14 -17
View File
@@ -12,13 +12,13 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^22.0.6", "@angular/cdk": "^22.0.6",
"@angular/common": "~22.0.8", "@angular/common": "~22.1.0",
"@angular/compiler": "~22.0.8", "@angular/compiler": "~22.1.0",
"@angular/core": "~22.0.8", "@angular/core": "~22.1.0",
"@angular/forms": "~22.0.8", "@angular/forms": "~22.1.0",
"@angular/localize": "~22.0.8", "@angular/localize": "~22.1.0",
"@angular/platform-browser": "~22.0.8", "@angular/platform-browser": "~22.1.0",
"@angular/router": "~22.0.8", "@angular/router": "~22.1.0",
"@ng-bootstrap/ng-bootstrap": "^21.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "^23.5.0", "@ng-select/ng-select": "^23.5.0",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
@@ -32,26 +32,24 @@
"ngx-device-detector": "^12.0.0", "ngx-device-detector": "^12.0.0",
"ngx-ui-tour-ng-bootstrap": "^19.0.0", "ngx-ui-tour-ng-bootstrap": "^19.0.0",
"normalize-diacritics": "^5.0.0", "normalize-diacritics": "^5.0.0",
"pdfjs-dist": "^6.0.227", "pdfjs-dist": "^6.2.108",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0", "utif": "^3.1.0",
"uuid": "^14.0.1" "uuid": "^14.0.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "^22.0.1",
"@angular-builders/jest": "^22.0.1", "@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.0.8", "@angular-devkit/core": "^22.1.2",
"@angular-devkit/schematics": "^22.0.8", "@angular-devkit/schematics": "^22.1.2",
"@angular-eslint/builder": "22.1.0", "@angular-eslint/builder": "22.1.0",
"@angular-eslint/eslint-plugin": "22.1.0", "@angular-eslint/eslint-plugin": "22.1.0",
"@angular-eslint/eslint-plugin-template": "22.1.0", "@angular-eslint/eslint-plugin-template": "22.1.0",
"@angular-eslint/schematics": "22.1.0", "@angular-eslint/schematics": "22.1.0",
"@angular-eslint/template-parser": "22.1.0", "@angular-eslint/template-parser": "22.1.0",
"@angular/build": "^22.0.8", "@angular/build": "22.1.2",
"@angular/cli": "~22.0.5", "@angular/cli": "22.1.2",
"@angular/compiler-cli": "~22.0.8", "@angular/compiler-cli": "~22.1.0",
"@codecov/webpack-plugin": "^2.0.1",
"@playwright/test": "^1.62.0", "@playwright/test": "^1.62.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.1.1", "@types/node": "^26.1.1",
@@ -66,8 +64,7 @@
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^6.0.3", "typescript": "^6.0.3"
"webpack": "^5.107.2"
}, },
"packageManager": "pnpm@10.26.0" "packageManager": "pnpm@10.26.0"
} }
+1810 -1797
View File
File diff suppressed because it is too large Load Diff
@@ -151,6 +151,13 @@
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
& section {
position: absolute;
text-align: initial;
box-sizing: border-box;
transform-origin: 0 0;
}
& .annotationTextContent { & .annotationTextContent {
opacity: 0; opacity: 0;
} }
@@ -13,6 +13,7 @@ import {
ViewChild, ViewChild,
} from '@angular/core' } from '@angular/core'
import { import {
AnnotationMode,
getDocument, getDocument,
GlobalWorkerOptions, GlobalWorkerOptions,
PDFDocumentLoadingTask, PDFDocumentLoadingTask,
@@ -221,6 +222,7 @@ export class PngxPdfViewerComponent
linkService: this.linkService, linkService: this.linkService,
findController: this.findController, findController: this.findController,
textLayerMode, textLayerMode,
annotationMode: AnnotationMode.ENABLE,
enableSelectionRendering: false, enableSelectionRendering: false,
removePageBorders: true, removePageBorders: true,
} }
@@ -2213,6 +2213,20 @@ describe('FilterEditorComponent', () => {
expect(blurSpy).toHaveBeenCalled() expect(blurSpy).toHaveBeenCalled()
}) })
it('should only dismiss open autocomplete suggestions on Escape, keeping the query', () => {
component.textFilter = 'foo bar'
component.textFilterInput.nativeElement.value = 'foo bar'
jest.spyOn(component.searchTypeahead, 'isPopupOpen').mockReturnValue(true)
const dismissSpy = jest
.spyOn(component.searchTypeahead, 'dismissPopup')
.mockImplementation(() => {})
component.textFilterInput.nativeElement.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape' })
)
expect(dismissSpy).toHaveBeenCalled()
expect(component.textFilter).toEqual('foo bar')
})
it('should adjust text filter targets if more like search', () => { it('should adjust text filter targets if more like search', () => {
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' // private const const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' // private const
component.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE component.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
@@ -15,6 +15,7 @@ import {
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { import {
NgbDropdownModule, NgbDropdownModule,
NgbTypeahead,
NgbTypeaheadModule, NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -351,6 +352,9 @@ export class FilterEditorComponent
@ViewChild('textFilterInput') @ViewChild('textFilterInput')
textFilterInput: ElementRef textFilterInput: ElementRef
@ViewChild(NgbTypeahead)
searchTypeahead: NgbTypeahead
readonly customFields = signal<CustomField[]>([]) readonly customFields = signal<CustomField[]>([])
tagDocumentCounts: SelectionDataItem[] tagDocumentCounts: SelectionDataItem[]
@@ -1150,6 +1154,7 @@ export class FilterEditorComponent
} }
set textFilter(value) { set textFilter(value) {
this._textFilter = value // set immediately to prevent loss of keystrokes
this.textFilterDebounce.next(value) this.textFilterDebounce.next(value)
} }
@@ -1242,9 +1247,9 @@ export class FilterEditorComponent
distinctUntilChanged(), distinctUntilChanged(),
filter((query) => !query.length || query.length > 2) filter((query) => !query.length || query.length > 2)
) )
.subscribe((text) => .subscribe(() =>
this.updateTextFilter( this.updateTextFilter(
text, this._textFilter, // use the current value, not the debounced (possibly stale) one
this.textFilterTarget !== TEXT_FILTER_TARGET_FULLTEXT_QUERY this.textFilterTarget !== TEXT_FILTER_TARGET_FULLTEXT_QUERY
) )
) )
@@ -1320,6 +1325,11 @@ export class FilterEditorComponent
this.updateTextFilter(filterString) this.updateTextFilter(filterString)
} }
} else if (event.key === 'Escape') { } else if (event.key === 'Escape') {
if (this.searchTypeahead?.isPopupOpen()) {
// only dismiss the suggestions, so longer query can use Enter
this.searchTypeahead.dismissPopup()
return
}
if (this._textFilter?.length) { if (this._textFilter?.length) {
this.resetTextField() this.resetTextField()
} else { } else {
@@ -88,7 +88,7 @@
@if (depth > 0) { @if (depth > 0) {
<div class="indicator"></div> <div class="indicator"></div>
} }
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button> <button class="btn btn-link ms-0 ps-0 text-start" style="user-select: text;" [disabled]="!userCanEdit(object)" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
</td> </td>
<td class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> <td class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
<td>{{ getDocumentCount(object) }}</td> <td>{{ getDocumentCount(object) }}</td>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,13 @@ export const GlobalWorkerOptions = {
workerSrc: '', workerSrc: '',
} }
export const AnnotationMode = {
DISABLE: 0,
ENABLE: 1,
ENABLE_FORMS: 2,
ENABLE_STORAGE: 3,
}
export const getDocument = (_src: unknown): PDFDocumentLoadingTask => { export const getDocument = (_src: unknown): PDFDocumentLoadingTask => {
return new PDFDocumentLoadingTask(Promise.resolve(new PDFDocumentProxy())) return new PDFDocumentLoadingTask(Promise.resolve(new PDFDocumentProxy()))
} }
View File
+346
View File
@@ -0,0 +1,346 @@
from __future__ import annotations
import abc
import hashlib
import json
import os
import shutil
import tempfile
import zipfile
from contextlib import AbstractContextManager
from contextlib import contextmanager
from pathlib import Path
from pathlib import PurePosixPath
from typing import TYPE_CHECKING
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from documents.file_handling import delete_empty_directories
from documents.utils import compute_checksum
from documents.utils import copy_file_with_basic_stats
if TYPE_CHECKING:
from collections.abc import Iterator
from typing import TextIO
def _dumps(content: list | dict) -> str:
"""Serialize export JSON consistently across all sinks."""
return json.dumps(content, cls=DjangoJSONEncoder, indent=2, ensure_ascii=False)
class StreamingManifestWriter:
"""Incrementally writes a JSON array to a text handle, one record at a time.
Knows nothing about folders or zips: it writes the array framing and records
to whatever handle the sink's ``stream()`` yields. The sink owns the handle's
lifecycle (atomic rename, compare, spooling).
"""
def __init__(self, handle: TextIO) -> None:
self._file = handle
self._first = True
self._file.write("[")
def write_record(self, record: dict) -> None:
if not self._first:
self._file.write(",\n")
else:
self._first = False
self._file.write(_dumps(record))
def write_batch(self, records: list[dict]) -> None:
for record in records:
self.write_record(record)
def close(self) -> None:
"""Write the closing bracket. Does NOT close the handle (the sink owns it)."""
self._file.write("\n]")
class ExportSink(AbstractContextManager, abc.ABC):
"""Destination for a document export.
The command declares export contents via three verbs; the sink decides how to
persist each. ``arcname`` is always a relative POSIX path
(e.g. ``"manifest.json"``, ``"originals/foo.pdf"``).
Contract:
* At most one ``stream()`` open at a time (it is the manifest);
``add_file``/``add_json`` may be called while it is open.
* Context-manager: normal exit finalizes, an exception aborts. No partial or
failed run leaves a complete-looking artifact.
"""
@abc.abstractmethod
def add_file(
self,
source: Path,
arcname: str,
*,
checksum: str | None = None,
) -> None: ...
@abc.abstractmethod
def add_json(self, content: list | dict, arcname: str) -> None: ...
@abc.abstractmethod
def stream(self, arcname: str) -> AbstractContextManager[TextIO]: ...
def _open(self) -> None:
"""Hook called on context entry. Override as needed."""
@abc.abstractmethod
def _finalize(self) -> None:
"""Commit on clean exit."""
@abc.abstractmethod
def _abort(self) -> None:
"""Roll back on exception."""
def __enter__(self) -> ExportSink:
self._open()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if exc_type is not None:
self._abort()
else:
self._finalize()
class DirectoryExportSink(ExportSink):
"""Writes loose files into a target directory, with incremental sync.
Owns the snapshot/skip/compare/prune machinery that used to live in the
command (``files_in_export_dir``, ``check_and_copy``, ``check_and_write_json``,
and the ``--delete`` pass).
"""
def __init__(
self,
target: Path,
*,
compare_checksums: bool,
compare_json: bool,
delete: bool,
) -> None:
self._target = target.resolve()
self._compare_checksums = compare_checksums
self._compare_json = compare_json
self._delete = delete
self._snapshot: set[Path] = set()
self._stream_open = False
def _open(self) -> None:
for x in self._target.glob("**/*"):
if x.is_file():
self._snapshot.add(x.resolve())
def add_file(
self,
source: Path,
arcname: str,
*,
checksum: str | None = None,
) -> None:
target = (self._target / arcname).resolve()
self._snapshot.discard(target)
perform_copy = False
if target.exists():
source_stat = source.stat()
target_stat = target.stat()
if self._compare_checksums and checksum:
perform_copy = compute_checksum(target) != checksum
elif (
source_stat.st_mtime != target_stat.st_mtime
or source_stat.st_size != target_stat.st_size
):
perform_copy = True
else:
perform_copy = True
if perform_copy:
target.parent.mkdir(parents=True, exist_ok=True)
copy_file_with_basic_stats(source, target)
@staticmethod
def _content_unchanged(target: Path, new_bytes: bytes) -> bool:
"""True if ``target`` already holds byte-identical content (BLAKE2b)."""
return (
hashlib.blake2b(target.read_bytes()).hexdigest()
== hashlib.blake2b(new_bytes).hexdigest()
)
def add_json(self, content: list | dict, arcname: str) -> None:
target = (self._target / arcname).resolve()
json_str = _dumps(content)
perform_write = True
if target in self._snapshot:
self._snapshot.discard(target)
if self._compare_json and self._content_unchanged(
target,
json_str.encode("utf-8"),
):
perform_write = False
if perform_write:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json_str, encoding="utf-8")
@contextmanager
def stream(self, arcname: str) -> Iterator[TextIO]:
if self._stream_open:
raise RuntimeError("A stream is already open on this sink")
target = (self._target / arcname).resolve()
tmp = target.with_suffix(target.suffix + ".tmp")
target.parent.mkdir(parents=True, exist_ok=True)
handle = tmp.open("w", encoding="utf-8")
self._stream_open = True
try:
yield handle
except BaseException:
handle.close()
tmp.unlink(missing_ok=True)
raise
else:
handle.close()
self._commit_streamed_file(target, tmp)
finally:
self._stream_open = False
def _commit_streamed_file(self, target: Path, tmp: Path) -> None:
if target in self._snapshot:
self._snapshot.discard(target)
if self._compare_json and self._content_unchanged(
target,
tmp.read_bytes(),
):
tmp.unlink()
return
tmp.rename(target)
def _finalize(self) -> None:
if self._delete:
for f in self._snapshot:
if not f.is_relative_to(self._target): # pragma: no cover
# Defense in depth: a symlink inside the export dir can
# resolve outside of it; never delete outside the target.
continue
f.unlink()
delete_empty_directories(f.parent, self._target)
def _abort(self) -> None:
# Folder mode is in-place/incremental: streamed .tmp files are already
# cleaned in stream(); leave everything else intact and skip the prune.
return None
class ZipExportSink(ExportSink):
"""Writes a single zip archive, produced atomically only on success.
Builds into ``<target>/<zip_name>.zip.tmp`` and renames to ``.zip`` on clean
finalize. The manifest stream is spooled to a temp file in SCRATCH_DIR and
added as an entry at finalize (a zip entry cannot be interleaved with others).
"""
def __init__(self, target: Path, zip_name: str, *, delete: bool = False) -> None:
self._target = target.resolve()
self._zip_path = (self._target / zip_name).with_suffix(".zip")
self._tmp_path = self._zip_path.with_name(self._zip_path.name + ".tmp")
self._delete = delete
self._zip: zipfile.ZipFile | None = None
self._dirs: set[str] = set()
self._pending_manifest: tuple[Path, str] | None = None
self._stream_open = False
def _open(self) -> None:
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
self._zip = zipfile.ZipFile(
self._tmp_path,
"w",
compression=zipfile.ZIP_DEFLATED,
allowZip64=True,
)
def _ensure_dirs(self, arcname: str) -> None:
assert self._zip is not None
dir_arc = ""
for part in PurePosixPath(arcname).parts[:-1]:
dir_arc += f"{part}/"
if dir_arc not in self._dirs:
self._dirs.add(dir_arc)
self._zip.mkdir(dir_arc)
def add_file(
self,
source: Path,
arcname: str,
*,
checksum: str | None = None,
) -> None:
assert self._zip is not None
self._ensure_dirs(arcname)
self._zip.write(source, arcname=arcname)
def add_json(self, content: list | dict, arcname: str) -> None:
assert self._zip is not None
self._ensure_dirs(arcname)
self._zip.writestr(arcname, _dumps(content))
@contextmanager
def stream(self, arcname: str) -> Iterator[TextIO]:
if self._stream_open:
raise RuntimeError("A stream is already open on this sink")
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
fd, tmp_name = tempfile.mkstemp(
dir=settings.SCRATCH_DIR,
prefix="export-manifest-",
suffix=".json",
)
tmp = Path(tmp_name)
handle = os.fdopen(fd, "w", encoding="utf-8")
self._stream_open = True
try:
yield handle
except BaseException:
handle.close()
tmp.unlink(missing_ok=True)
raise
else:
handle.close()
self._pending_manifest = (tmp, arcname)
finally:
self._stream_open = False
def _finalize(self) -> None:
assert self._zip is not None
if self._pending_manifest is not None:
tmp, arcname = self._pending_manifest
self._ensure_dirs(arcname)
self._zip.write(tmp, arcname=arcname)
tmp.unlink(missing_ok=True)
self._pending_manifest = None
self._zip.close()
self._zip = None
if self._delete:
self._wipe_destination()
self._tmp_path.replace(self._zip_path)
def _wipe_destination(self) -> None:
skip = {self._zip_path.resolve(), self._tmp_path.resolve()}
for item in self._target.glob("*"):
if item.resolve() in skip:
continue
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
def _abort(self) -> None:
if self._zip is not None:
self._zip.close()
self._zip = None
self._tmp_path.unlink(missing_ok=True)
if self._pending_manifest is not None:
self._pending_manifest[0].unlink(missing_ok=True)
self._pending_manifest = None
+24 -49
View File
@@ -39,7 +39,6 @@ from guardian.utils import get_user_obj_perms_model
from rest_framework import serializers from rest_framework import serializers
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from rest_framework_guardian.filters import ObjectPermissionsFilter
from documents.models import Correspondent from documents.models import Correspondent
from documents.models import CustomField from documents.models import CustomField
@@ -51,7 +50,7 @@ from documents.models import ShareLink
from documents.models import ShareLinkBundle from documents.models import ShareLinkBundle
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.permissions import permitted_document_ids from documents.permissions import permitted_object_ids
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
@@ -1028,59 +1027,35 @@ class PaperlessTaskFilterSet(FilterSet):
return queryset.exclude(status__in=PaperlessTask.COMPLETE_STATUSES) return queryset.exclude(status__in=PaperlessTask.COMPLETE_STATUSES)
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter): class PermittedObjectsFilter(BaseFilterBackend):
""" """
A filter backend that limits results to those where the requesting user Filters a queryset down to objects the requesting user owns, are
has read object level permissions, owns the objects, or objects without unowned, or (when ``include_granted`` is True) has an explicit
an owner (for backwards compat) user/group guardian permission on. Backed by ``permitted_object_ids``
-- a single ``id__in`` subquery, not a join -- so it can't produce
duplicate rows even when the base queryset already carries independent
joins (e.g. multi-value ``tags__id__all`` filtering), and stays
index-friendly at scale instead of falling back to guardian's
varchar-cast join.
Set ``include_granted = False`` on a subclass for endpoints that
intentionally only show owned/unowned objects regardless of explicit
shares (e.g. ``TrashView``).
""" """
include_granted: bool = True
perm_codename: str | None = None
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
if request.user.is_superuser: if request.user.is_superuser:
return queryset return queryset
objects_with_perms = super().filter_queryset(request, queryset, view) if not self.include_granted:
objects_owned = queryset.filter(owner=request.user) return queryset.filter(Q(owner=request.user) | Q(owner__isnull=True))
objects_unowned = queryset.filter(owner__isnull=True) model = queryset.model
return objects_with_perms | objects_owned | objects_unowned perm = self.perm_codename or f"view_{model._meta.model_name}"
return queryset.filter(
id__in=permitted_object_ids(request.user, model, perm),
class DocumentPermissionsFilter(BaseFilterBackend): )
"""
A filter backend limiting Document results to those the requesting user
owns, are unowned, or has explicit (user- or group-level) view
permission on.
Unlike ``ObjectOwnedOrGrantedPermissionsFilter``, this does not build an
``objects_with_perms | objects_owned | objects_unowned`` union of
querysets derived from the same base queryset. When that base queryset
already carries independent joins on a multi-valued relation (e.g. two
separate joins from ``tags__id__all`` filtering on two tags), each
OR-ed branch can end up pairing those joins' aliases differently,
letting more than one row out of the join's cross product satisfy the
combined WHERE -- returning the same document more than once. Filtering
via a single ``id__in`` against ``permitted_document_ids`` (a plain
subquery, not a join) sidesteps that entirely and is also cheaper than
guardian's join-based permission check.
"""
def filter_queryset(self, request, queryset, view):
if request.user.is_superuser:
return queryset
return queryset.filter(id__in=permitted_document_ids(request.user))
class ObjectOwnedPermissionsFilter(ObjectPermissionsFilter):
"""
A filter backend that limits results to those where the requesting user
owns the objects or objects without an owner (for backwards compat)
"""
def filter_queryset(self, request, queryset, view):
if request.user.is_superuser:
return queryset
objects_owned = queryset.filter(owner=request.user)
objects_unowned = queryset.filter(owner__isnull=True)
return objects_owned | objects_unowned
class DocumentsOrderingFilter(OrderingFilter): class DocumentsOrderingFilter(OrderingFilter):
@@ -1,8 +1,4 @@
import hashlib
import json
import os import os
import shutil
import tempfile
from itertools import islice from itertools import islice
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -19,7 +15,6 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core import serializers from django.core import serializers
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from filelock import FileLock from filelock import FileLock
@@ -34,7 +29,10 @@ if TYPE_CHECKING:
if settings.AUDIT_LOG_ENABLED: if settings.AUDIT_LOG_ENABLED:
from auditlog.models import LogEntry from auditlog.models import LogEntry
from documents.file_handling import delete_empty_directories from documents.export.sinks import DirectoryExportSink
from documents.export.sinks import ExportSink
from documents.export.sinks import StreamingManifestWriter
from documents.export.sinks import ZipExportSink
from documents.file_handling import generate_filename from documents.file_handling import generate_filename
from documents.management.commands.base import PaperlessCommand from documents.management.commands.base import PaperlessCommand
from documents.management.commands.mixins import CryptMixin from documents.management.commands.mixins import CryptMixin
@@ -60,8 +58,7 @@ from documents.settings import EXPORTER_ARCHIVE_NAME
from documents.settings import EXPORTER_FILE_NAME from documents.settings import EXPORTER_FILE_NAME
from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME
from documents.settings import EXPORTER_THUMBNAIL_NAME from documents.settings import EXPORTER_THUMBNAIL_NAME
from documents.utils import compute_checksum from documents.utils import QuerySetStream
from documents.utils import copy_file_with_basic_stats
from paperless import version from paperless import version
from paperless.models import ApplicationConfiguration from paperless.models import ApplicationConfiguration
from paperless_mail.models import MailAccount from paperless_mail.models import MailAccount
@@ -84,87 +81,6 @@ def serialize_queryset_batched(
yield serializers.serialize("python", chunk) yield serializers.serialize("python", chunk)
class StreamingManifestWriter:
"""Incrementally writes a JSON array to a file, one record at a time.
Writes to <target>.tmp first; on close(), optionally BLAKE2b-compares
with the existing file (--compare-json) and renames or discards accordingly.
On exception, discard() deletes the tmp file and leaves the original intact.
"""
def __init__(
self,
path: Path,
*,
compare_json: bool = False,
files_in_export_dir: "set[Path] | None" = None,
) -> None:
self._path = path.resolve()
self._tmp_path = self._path.with_suffix(self._path.suffix + ".tmp")
self._compare_json = compare_json
self._files_in_export_dir: set[Path] = (
files_in_export_dir if files_in_export_dir is not None else set()
)
self._file = None
self._first = True
def open(self) -> None:
self._path.parent.mkdir(parents=True, exist_ok=True)
self._file = self._tmp_path.open("w", encoding="utf-8")
self._file.write("[")
self._first = True
def write_record(self, record: dict) -> None:
if not self._first:
self._file.write(",\n")
else:
self._first = False
self._file.write(
json.dumps(record, cls=DjangoJSONEncoder, indent=2, ensure_ascii=False),
)
def write_batch(self, records: list[dict]) -> None:
for record in records:
self.write_record(record)
def close(self) -> None:
if self._file is None:
return
self._file.write("\n]")
self._file.close()
self._file = None
self._finalize()
def discard(self) -> None:
if self._file is not None:
self._file.close()
self._file = None
if self._tmp_path.exists():
self._tmp_path.unlink()
def _finalize(self) -> None:
"""Compare with existing file (if --compare-json) then rename or discard tmp."""
if self._path in self._files_in_export_dir:
self._files_in_export_dir.remove(self._path)
if self._compare_json:
existing_hash = hashlib.blake2b(self._path.read_bytes()).hexdigest()
new_hash = hashlib.blake2b(self._tmp_path.read_bytes()).hexdigest()
if existing_hash == new_hash:
self._tmp_path.unlink()
return
self._tmp_path.rename(self._path)
def __enter__(self) -> "StreamingManifestWriter":
self.open()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if exc_type is not None:
self.discard()
else:
self.close()
class Command(CryptMixin, PaperlessCommand): class Command(CryptMixin, PaperlessCommand):
help = ( help = (
"Decrypt and rename all files in our collection into a given target " "Decrypt and rename all files in our collection into a given target "
@@ -314,20 +230,13 @@ class Command(CryptMixin, PaperlessCommand):
self.passphrase: str | None = options.get("passphrase") self.passphrase: str | None = options.get("passphrase")
self.batch_size: int = options["batch_size"] self.batch_size: int = options["batch_size"]
self.files_in_export_dir: set[Path] = set()
self.exported_files: set[str] = set() self.exported_files: set[str] = set()
# If zipping, save the original target for later and if self.zip_export and (self.compare_checksums or self.compare_json):
# get a temporary directory for the target instead raise CommandError(
temp_dir = None "--compare-checksums and --compare-json have no effect when "
self.original_target = self.target "used with --zip",
if self.zip_export:
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
temp_dir = tempfile.TemporaryDirectory(
dir=settings.SCRATCH_DIR,
prefix="paperless-export",
) )
self.target = Path(temp_dir.name).resolve()
if not self.target.exists(): if not self.target.exists():
raise CommandError("That path doesn't exist") raise CommandError("That path doesn't exist")
@@ -338,33 +247,28 @@ class Command(CryptMixin, PaperlessCommand):
if not os.access(self.target, os.W_OK): if not os.access(self.target, os.W_OK):
raise CommandError("That path doesn't appear to be writable") raise CommandError("That path doesn't appear to be writable")
try: sink: ExportSink
# Prevent any ongoing changes in the documents if self.zip_export:
with FileLock(settings.MEDIA_LOCK): sink = ZipExportSink(
self.dump() self.target,
options["zip_name"],
delete=self.delete,
)
else:
sink = DirectoryExportSink(
self.target,
compare_checksums=self.compare_checksums,
compare_json=self.compare_json,
delete=self.delete,
)
# We've written everything to the temporary directory in this case, # Prevent any ongoing changes in the documents while exporting
# now make an archive in the original target, with all files stored with FileLock(settings.MEDIA_LOCK), sink:
if self.zip_export and temp_dir is not None: self.dump(sink)
shutil.make_archive(
self.original_target / options["zip_name"],
format="zip",
root_dir=temp_dir.name,
)
finally: def dump(self, sink: ExportSink) -> None:
# Always cleanup the temporary directory, if one was created # 1. Create manifest, containing all correspondents, types, tags, storage
if self.zip_export and temp_dir is not None: # paths, note, documents and ui_settings
temp_dir.cleanup()
def dump(self) -> None:
# 1. Take a snapshot of what files exist in the current export folder
for x in self.target.glob("**/*"):
if x.is_file():
self.files_in_export_dir.add(x.resolve())
# 2. Create manifest, containing all correspondents, types, tags, storage paths
# note, documents and ui_settings
_excluded_usernames = ["consumer", "AnonymousUser"] _excluded_usernames = ["consumer", "AnonymousUser"]
manifest_key_to_object_query: dict[str, QuerySet[Any]] = { manifest_key_to_object_query: dict[str, QuerySet[Any]] = {
"correspondents": Correspondent.objects.all(), "correspondents": Correspondent.objects.all(),
@@ -427,13 +331,9 @@ class Command(CryptMixin, PaperlessCommand):
document_manifest: list[dict] = [] document_manifest: list[dict] = []
share_link_bundle_manifest: list[dict] = [] share_link_bundle_manifest: list[dict] = []
manifest_path = (self.target / "manifest.json").resolve()
with StreamingManifestWriter( with sink.stream("manifest.json") as handle:
manifest_path, writer = StreamingManifestWriter(handle)
compare_json=self.compare_json,
files_in_export_dir=self.files_in_export_dir,
) as writer:
with transaction.atomic(): with transaction.atomic():
for key, qs in manifest_key_to_object_query.items(): for key, qs in manifest_key_to_object_query.items():
if key == "documents": if key == "documents":
@@ -469,9 +369,6 @@ class Command(CryptMixin, PaperlessCommand):
self._encrypt_record_inline(record) self._encrypt_record_inline(record)
writer.write_batch(batch) writer.write_batch(batch)
document_map: dict[int, Document] = {
d.pk: d for d in Document.global_objects.order_by("id")
}
share_link_bundle_map: dict[int, ShareLinkBundle] = { share_link_bundle_map: dict[int, ShareLinkBundle] = {
b.pk: b b.pk: b
for b in ShareLinkBundle.objects.order_by("id").prefetch_related( for b in ShareLinkBundle.objects.order_by("id").prefetch_related(
@@ -479,84 +376,72 @@ class Command(CryptMixin, PaperlessCommand):
) )
} }
# 3. Export files from each document # 2. Export files from each document
for index, document_dict in enumerate( # document_manifest and this stream are both ordered by id from the
self.track( # same underlying rows, so zip them in lockstep instead of building
document_manifest, # a dict of every Document instance up front (QuerySetStream keeps
description="Exporting documents...", # only one batch of documents resident at a time).
total=len(document_manifest), documents_stream = QuerySetStream(
), Document.global_objects.order_by("id"),
chunk_size=self.batch_size,
)
for document_dict, document in self.track(
zip(document_manifest, documents_stream, strict=True),
description="Exporting documents...",
total=len(document_manifest),
): ):
document = document_map[document_dict["pk"]] # Both document_manifest and documents_stream come from the same
# Document.global_objects.order_by("id") query, taken while
# MEDIA_LOCK is held, so this should be unreachable -- it guards
# against silent data corruption if that invariant ever breaks.
if document.pk != document_dict["pk"]: # pragma: no cover
raise CommandError(
"Document export ordering mismatch: expected "
f"pk={document_dict['pk']}, got pk={document.pk}. "
"Documents may have changed during export.",
)
# 3.1. generate a unique filename # generate a unique filename, then the arcnames for its files
base_name = self.generate_base_name(document) base_name = self.generate_base_name(document)
original_arc, thumbnail_arc, archive_arc = (
# 3.2. write filenames into manifest
original_target, thumbnail_target, archive_target = (
self.generate_document_targets(document, base_name, document_dict) self.generate_document_targets(document, base_name, document_dict)
) )
# 3.3. write files to target folder
if not self.data_only: if not self.data_only:
self.copy_document_files( self.copy_document_files(
document, document,
original_target, sink,
thumbnail_target, original_arc,
archive_target, thumbnail_arc,
archive_arc,
) )
if self.split_manifest: if self.split_manifest:
self._write_split_manifest(document_dict, document, base_name) self._write_split_manifest(sink, document_dict, document, base_name)
else: else:
writer.write_record(document_dict) writer.write_record(document_dict)
for bundle_dict in share_link_bundle_manifest: for bundle_dict in share_link_bundle_manifest:
bundle = share_link_bundle_map[bundle_dict["pk"]] bundle = share_link_bundle_map[bundle_dict["pk"]]
bundle_arc = self.generate_share_link_bundle_target(
bundle_target = self.generate_share_link_bundle_target(
bundle, bundle,
bundle_dict, bundle_dict,
) )
if not self.data_only and bundle_arc is not None:
if not self.data_only and bundle_target is not None: self.copy_share_link_bundle_file(bundle, sink, bundle_arc)
self.copy_share_link_bundle_file(bundle, bundle_target)
writer.write_record(bundle_dict) writer.write_record(bundle_dict)
# 4.2 write version information to target folder writer.close()
extra_metadata_path = (self.target / "metadata.json").resolve()
# 3. Write version (and crypto params) to metadata.json
# Django stores most crypto values in the field itself; we store
# them once here for the whole export
metadata: dict[str, str | int | dict[str, str | int]] = { metadata: dict[str, str | int | dict[str, str | int]] = {
"version": version.__full_version_str__, "version": version.__full_version_str__,
} }
# 4.2.1 If needed, write the crypto values into the metadata
# Django stores most of these in the field itself, we store them once here
if self.passphrase: if self.passphrase:
metadata.update(self.get_crypt_params()) metadata.update(self.get_crypt_params())
sink.add_json(metadata, "metadata.json")
self.check_and_write_json(
metadata,
extra_metadata_path,
)
if self.delete:
# 5. Remove files which we did not explicitly export in this run
if not self.zip_export:
for f in self.files_in_export_dir:
f.unlink()
delete_empty_directories(
f.parent,
self.target,
)
else:
# 5. Remove anything in the original location (before moving the zip)
for item in self.original_target.glob("*"):
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
def generate_base_name(self, document: Document) -> Path: def generate_base_name(self, document: Document) -> Path:
""" """
@@ -584,73 +469,69 @@ class Command(CryptMixin, PaperlessCommand):
document: Document, document: Document,
base_name: Path, base_name: Path,
document_dict: dict, document_dict: dict,
) -> tuple[Path, Path | None, Path | None]: ) -> tuple[str, str | None, str | None]:
""" """
Generates the targets for a given document, including the original file, archive file and thumbnail (depending on settings). Generates the relative POSIX arcnames for a document's original, thumbnail
and archive files (depending on settings), and records them in the manifest.
""" """
original_name = base_name original_name = base_name
if self.use_folder_prefix: if self.use_folder_prefix:
original_name = Path("originals") / original_name original_name = Path("originals") / original_name
original_target = (self.target / original_name).resolve() original_arc = original_name.as_posix()
document_dict[EXPORTER_FILE_NAME] = str(original_name) document_dict[EXPORTER_FILE_NAME] = original_arc
if not self.no_thumbnail: if not self.no_thumbnail:
thumbnail_name = base_name.parent / (base_name.stem + "-thumbnail.webp") thumbnail_name = base_name.parent / (base_name.stem + "-thumbnail.webp")
if self.use_folder_prefix: if self.use_folder_prefix:
thumbnail_name = Path("thumbnails") / thumbnail_name thumbnail_name = Path("thumbnails") / thumbnail_name
thumbnail_target = (self.target / thumbnail_name).resolve() thumbnail_arc = thumbnail_name.as_posix()
document_dict[EXPORTER_THUMBNAIL_NAME] = str(thumbnail_name) document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_arc
else: else:
thumbnail_target = None thumbnail_arc = None
if not self.no_archive and document.has_archive_version: if not self.no_archive and document.has_archive_version:
archive_name = base_name.parent / (base_name.stem + "-archive.pdf") archive_name = base_name.parent / (base_name.stem + "-archive.pdf")
if self.use_folder_prefix: if self.use_folder_prefix:
archive_name = Path("archive") / archive_name archive_name = Path("archive") / archive_name
archive_target = (self.target / archive_name).resolve() archive_arc = archive_name.as_posix()
document_dict[EXPORTER_ARCHIVE_NAME] = str(archive_name) document_dict[EXPORTER_ARCHIVE_NAME] = archive_arc
else: else:
archive_target = None archive_arc = None
return original_target, thumbnail_target, archive_target return original_arc, thumbnail_arc, archive_arc
def copy_document_files( def copy_document_files(
self, self,
document: Document, document: Document,
original_target: Path, sink: ExportSink,
thumbnail_target: Path | None, original_arc: str,
archive_target: Path | None, thumbnail_arc: str | None,
archive_arc: str | None,
) -> None: ) -> None:
""" """
Copies files from the document storage location to the specified target location. Hands the document's files to the sink (original, thumbnail, archive).
If the document is encrypted, the files are decrypted before copying them to the target location.
""" """
self.check_and_copy( sink.add_file(document.source_path, original_arc, checksum=document.checksum)
document.source_path,
document.checksum,
original_target,
)
if thumbnail_target: if thumbnail_arc:
self.check_and_copy(document.thumbnail_path, None, thumbnail_target) sink.add_file(document.thumbnail_path, thumbnail_arc)
if archive_target: if archive_arc:
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(document.archive_path, Path) assert isinstance(document.archive_path, Path)
self.check_and_copy( sink.add_file(
document.archive_path, document.archive_path,
document.archive_checksum, archive_arc,
archive_target, checksum=document.archive_checksum,
) )
def generate_share_link_bundle_target( def generate_share_link_bundle_target(
self, self,
bundle: ShareLinkBundle, bundle: ShareLinkBundle,
bundle_dict: dict, bundle_dict: dict,
) -> Path | None: ) -> str | None:
""" """
Generates the export target for a share link bundle file, when present. Generates the relative POSIX arcname for a share link bundle file, if any.
""" """
if not bundle.file_path: if not bundle.file_path:
return None return None
@@ -666,25 +547,22 @@ class Command(CryptMixin, PaperlessCommand):
bundle_dict["fields"]["file_path"] = portable_bundle_path.as_posix() bundle_dict["fields"]["file_path"] = portable_bundle_path.as_posix()
bundle_dict[EXPORTER_SHARE_LINK_BUNDLE_NAME] = export_bundle_path.as_posix() bundle_dict[EXPORTER_SHARE_LINK_BUNDLE_NAME] = export_bundle_path.as_posix()
return (self.target / export_bundle_path).resolve() return export_bundle_path.as_posix()
def copy_share_link_bundle_file( def copy_share_link_bundle_file(
self, self,
bundle: ShareLinkBundle, bundle: ShareLinkBundle,
bundle_target: Path, sink: ExportSink,
bundle_arc: str,
) -> None: ) -> None:
""" """
Copies a share link bundle ZIP into the export directory. Hands a share link bundle ZIP to the sink.
""" """
bundle_source_path = bundle.absolute_file_path bundle_source_path = bundle.absolute_file_path
if bundle_source_path is None: if bundle_source_path is None:
raise FileNotFoundError(f"Share link bundle {bundle.pk} has no file path") raise FileNotFoundError(f"Share link bundle {bundle.pk} has no file path")
self.check_and_copy( sink.add_file(bundle_source_path, bundle_arc)
bundle_source_path,
None,
bundle_target,
)
def _encrypt_record_inline(self, record: dict) -> None: def _encrypt_record_inline(self, record: dict) -> None:
"""Encrypt sensitive fields in a single record, if passphrase is set.""" """Encrypt sensitive fields in a single record, if passphrase is set."""
@@ -700,6 +578,7 @@ class Command(CryptMixin, PaperlessCommand):
def _write_split_manifest( def _write_split_manifest(
self, self,
sink: ExportSink,
document_dict: dict, document_dict: dict,
document: Document, document: Document,
base_name: Path, base_name: Path,
@@ -721,81 +600,4 @@ class Command(CryptMixin, PaperlessCommand):
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json") manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
if self.use_folder_prefix: if self.use_folder_prefix:
manifest_name = Path("json") / manifest_name manifest_name = Path("json") / manifest_name
manifest_name = (self.target / manifest_name).resolve() sink.add_json(content, manifest_name.as_posix())
manifest_name.parent.mkdir(parents=True, exist_ok=True)
self.check_and_write_json(content, manifest_name)
def check_and_write_json(
self,
content: list[dict] | dict,
target: Path,
) -> None:
"""
Writes the source content to the target json file.
If --compare-json arg was used, don't write to target file if
the file exists and checksum is identical to content checksum.
This preserves the file timestamps when no changes are made.
"""
target = target.resolve()
perform_write = True
if target in self.files_in_export_dir:
self.files_in_export_dir.remove(target)
if self.compare_json:
target_checksum = hashlib.blake2b(target.read_bytes()).hexdigest()
src_str = json.dumps(
content,
cls=DjangoJSONEncoder,
indent=2,
ensure_ascii=False,
)
src_checksum = hashlib.blake2b(src_str.encode("utf-8")).hexdigest()
if src_checksum == target_checksum:
perform_write = False
if perform_write:
target.write_text(
json.dumps(
content,
cls=DjangoJSONEncoder,
indent=2,
ensure_ascii=False,
),
encoding="utf-8",
)
def check_and_copy(
self,
source: Path,
source_checksum: str | None,
target: Path,
) -> None:
"""
Copies the source to the target, if target doesn't exist or the target doesn't seem to match
the source attributes
"""
target = target.resolve()
if target in self.files_in_export_dir:
self.files_in_export_dir.remove(target)
perform_copy = False
if target.exists():
source_stat = source.stat()
target_stat = target.stat()
if self.compare_checksums and source_checksum:
target_checksum = compute_checksum(target)
perform_copy = target_checksum != source_checksum
elif (
source_stat.st_mtime != target_stat.st_mtime
or source_stat.st_size != target_stat.st_size
):
perform_copy = True
else:
# Copy if it does not exist
perform_copy = True
if perform_copy:
target.parent.mkdir(parents=True, exist_ok=True)
copy_file_with_basic_stats(source, target)
+10 -14
View File
@@ -19,7 +19,7 @@ from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import Workflow from documents.models import Workflow
from documents.models import WorkflowTrigger from documents.models import WorkflowTrigger
from documents.permissions import get_objects_for_user_owner_aware from documents.permissions import permitted_object_ids
from documents.regex import safe_regex_search from documents.regex import safe_regex_search
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -55,10 +55,8 @@ def match_correspondents(document: Document, classifier: DocumentClassifier, use
user = document.owner user = document.owner
if user is not None: if user is not None:
correspondents = get_objects_for_user_owner_aware( correspondents = Correspondent.objects.filter(
user, id__in=permitted_object_ids(user, Correspondent, "view_correspondent"),
"documents.view_correspondent",
Correspondent,
) )
else: else:
correspondents = Correspondent.objects.all() correspondents = Correspondent.objects.all()
@@ -86,10 +84,8 @@ def match_document_types(document: Document, classifier: DocumentClassifier, use
user = document.owner user = document.owner
if user is not None: if user is not None:
document_types = get_objects_for_user_owner_aware( document_types = DocumentType.objects.filter(
user, id__in=permitted_object_ids(user, DocumentType, "view_documenttype"),
"documents.view_documenttype",
DocumentType,
) )
else: else:
document_types = DocumentType.objects.all() document_types = DocumentType.objects.all()
@@ -116,7 +112,9 @@ def match_tags(document: Document, classifier: DocumentClassifier, user=None):
user = document.owner user = document.owner
if user is not None: if user is not None:
tags = get_objects_for_user_owner_aware(user, "documents.view_tag", Tag) tags = Tag.objects.filter(
id__in=permitted_object_ids(user, Tag, "view_tag"),
)
else: else:
tags = Tag.objects.all() tags = Tag.objects.all()
@@ -145,10 +143,8 @@ def match_storage_paths(document: Document, classifier: DocumentClassifier, user
user = document.owner user = document.owner
if user is not None: if user is not None:
storage_paths = get_objects_for_user_owner_aware( storage_paths = StoragePath.objects.filter(
user, id__in=permitted_object_ids(user, StoragePath, "view_storagepath"),
"documents.view_storagepath",
StoragePath,
) )
else: else:
storage_paths = StoragePath.objects.all() storage_paths = StoragePath.objects.all()
+59 -25
View File
@@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Case from django.db.models import Case
from django.db.models import Count from django.db.models import Count
from django.db.models import IntegerField from django.db.models import IntegerField
from django.db.models import Model
from django.db.models import Q from django.db.models import Q
from django.db.models import QuerySet from django.db.models import QuerySet
from django.db.models import Value from django.db.models import Value
@@ -163,30 +164,32 @@ def set_permissions_for_object(
) )
def permitted_document_ids( def permitted_object_ids(
user, user: User | None,
model: type[Model],
perm: str,
*, *,
perm: str = "view_document",
include_deleted: bool = False, include_deleted: bool = False,
): ) -> QuerySet[int]:
""" """
Return a queryset of document IDs the user has ``perm`` on (default Generic version of ``permitted_document_ids`` for any model with an
``"view_document"``). By default limited to non-deleted documents; pass ``owner`` field and guardian object-level permissions. ``include_deleted``
``include_deleted=True`` for callers that need to check permission on only has an effect for models exposing a ``global_objects``/``deleted_at``
soft-deleted documents (e.g. trash restore). This intentionally avoids soft-delete pattern (currently only ``Document``); for every other model
``get_objects_for_user`` to keep the subquery small and index-friendly. it is accepted but has no effect, since those models have no soft-delete
concept.
""" """
has_soft_delete = hasattr(model, "global_objects")
manager = Document.global_objects if include_deleted else Document.objects manager = (
base_docs = manager.all() model.global_objects if include_deleted and has_soft_delete else model.objects
base_docs = base_docs.only("id", "owner") )
base_qs = manager.all().only("id", "owner")
if user is None or not getattr(user, "is_authenticated", False): if user is None or not getattr(user, "is_authenticated", False):
# Just Anonymous user e.g. for drf-spectacular return base_qs.filter(owner__isnull=True).values_list("id", flat=True)
return base_docs.filter(owner__isnull=True).values_list("id", flat=True)
if getattr(user, "is_superuser", False): if getattr(user, "is_superuser", False):
return base_docs.values_list("id", flat=True) return base_qs.values_list("id", flat=True)
# Guardian's UserObjectPermission/GroupObjectPermission always store a bare # Guardian's UserObjectPermission/GroupObjectPermission always store a bare
# codename, but has_perm()-style callers commonly pass the qualified # codename, but has_perm()-style callers commonly pass the qualified
@@ -194,31 +197,46 @@ def permitted_document_ids(
# codename, so just drop any prefix rather than silently under-permitting. # codename, so just drop any prefix rather than silently under-permitting.
perm = perm.rsplit(".", 1)[-1] perm = perm.rsplit(".", 1)[-1]
document_ct = ContentType.objects.get_for_model(Document) content_type = ContentType.objects.get_for_model(model)
perm_filter = { perm_filter = {
"permission__codename": perm, "permission__codename": perm,
"permission__content_type": document_ct, "permission__content_type": content_type,
} }
user_perm_docs = ( user_perm_ids = (
UserObjectPermission.objects.filter(user=user, **perm_filter) UserObjectPermission.objects.filter(user=user, **perm_filter)
.annotate(object_pk_int=Cast("object_pk", IntegerField())) .annotate(object_pk_int=Cast("object_pk", IntegerField()))
.values_list("object_pk_int", flat=True) .values_list("object_pk_int", flat=True)
) )
group_perm_ids = (
group_perm_docs = (
GroupObjectPermission.objects.filter(group__user=user, **perm_filter) GroupObjectPermission.objects.filter(group__user=user, **perm_filter)
.annotate(object_pk_int=Cast("object_pk", IntegerField())) .annotate(object_pk_int=Cast("object_pk", IntegerField()))
.values_list("object_pk_int", flat=True) .values_list("object_pk_int", flat=True)
) )
permitted_ids = user_perm_ids.union(group_perm_ids)
permitted_documents = user_perm_docs.union(group_perm_docs) return base_qs.filter(
Q(owner=user) | Q(owner__isnull=True) | Q(id__in=permitted_ids),
return base_docs.filter(
Q(owner=user) | Q(owner__isnull=True) | Q(id__in=permitted_documents),
).values_list("id", flat=True) ).values_list("id", flat=True)
def permitted_document_ids(
user: User | None,
*,
perm: str = "view_document",
include_deleted: bool = False,
) -> QuerySet[int]:
"""
Document-specific convenience wrapper around ``permitted_object_ids``.
Return a queryset of document IDs the user has ``perm`` on (default
``"view_document"``). By default limited to non-deleted documents; pass
``include_deleted=True`` for callers that need to check permission on
soft-deleted documents (e.g. trash restore). This intentionally avoids
``get_objects_for_user`` to keep the subquery small and index-friendly.
"""
return permitted_object_ids(user, Document, perm, include_deleted=include_deleted)
def get_document_count_filter_for_user(user, related_name: str = "documents"): def get_document_count_filter_for_user(user, related_name: str = "documents"):
""" """
Return the Q object used to filter document counts for the given user. Return the Q object used to filter document counts for the given user.
@@ -341,6 +359,13 @@ def get_objects_for_user_owner_aware(
""" """
Returns objects the user owns, are unowned, or has explicit perms. Returns objects the user owns, are unowned, or has explicit perms.
When include_deleted is True, soft-deleted items are also included. When include_deleted is True, soft-deleted items are also included.
Legacy slow path (guardian-backed, O(n) style permission resolution).
Most queryset-filtering call sites have migrated onto
``PermittedObjectsFilter``/``permitted_object_ids()``, but this function
is kept because production callers still remain. Several callers remain
across ``documents/``, ``paperless_mail/``, and ``paperless_ai/`` --
grep for this function name before removing it.
""" """
manager = ( manager = (
Model.global_objects Model.global_objects
@@ -360,6 +385,15 @@ def get_objects_for_user_owner_aware(
def has_perms_owner_aware(user, perms, obj): def has_perms_owner_aware(user, perms, obj):
"""
Legacy slow path (guardian-backed) single-object permission check.
The queryset-filtering side of this migrated onto
``PermittedObjectsFilter``/``permitted_object_ids()``, but this
single-object check still has many production callers. Several callers
remain across ``documents/``, ``paperless_mail/``, and ``paperless_ai/``
-- grep for this function name before removing it.
"""
checker = ObjectPermissionChecker(user) checker = ObjectPermissionChecker(user)
return obj.owner is None or obj.owner == user or checker.has_perm(perms, obj) return obj.owner is None or obj.owner == user or checker.has_perm(perms, obj)
+2 -3
View File
@@ -70,8 +70,7 @@
] ]
</script> </script>
</pngx-root> </pngx-root>
<script src="{% static runtime_js %}" defer></script> <script src="{% static polyfills_js %}" type="module"></script>
<script src="{% static polyfills_js %}" defer></script> <script src="{% static main_js %}" type="module"></script>
<script src="{% static main_js %}" defer></script>
</body> </body>
</html> </html>
+327
View File
@@ -0,0 +1,327 @@
import io
import json
import os
import zipfile
from pathlib import Path
import pytest
from pytest_django.fixtures import SettingsWrapper
from documents.export.sinks import DirectoryExportSink
from documents.export.sinks import ExportSink
from documents.export.sinks import StreamingManifestWriter
from documents.export.sinks import ZipExportSink
from documents.export.sinks import _dumps
@pytest.fixture()
def source_file(tmp_path: Path) -> Path:
src: Path = tmp_path / "src" / "doc.pdf"
src.parent.mkdir(parents=True)
src.write_bytes(b"PDF-CONTENT")
return src
class TestDumps:
def test_dumps_is_indented_unicode_json(self) -> None:
result: str = _dumps({"a": "é", "b": 1})
assert '"é"' in result # ensure_ascii=False keeps unicode literal
assert "\n" in result # indent=2 produces newlines
assert json.loads(result) == {"a": "é", "b": 1}
class TestStreamingManifestWriter:
def test_writes_json_array_of_records(self) -> None:
handle: io.StringIO = io.StringIO()
writer: StreamingManifestWriter = StreamingManifestWriter(handle)
writer.write_batch([{"pk": 1}, {"pk": 2}])
writer.write_record({"pk": 3})
writer.close()
assert json.loads(handle.getvalue()) == [{"pk": 1}, {"pk": 2}, {"pk": 3}]
def test_empty_manifest_is_valid_empty_array(self) -> None:
handle: io.StringIO = io.StringIO()
writer: StreamingManifestWriter = StreamingManifestWriter(handle)
writer.close()
assert json.loads(handle.getvalue()) == []
class TestDirectoryExportSink:
def test_add_file_copies_to_relative_arcname(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=False,
) as sink:
sink.add_file(source_file, "originals/doc.pdf")
assert (target / "originals" / "doc.pdf").read_bytes() == b"PDF-CONTENT"
def test_add_json_writes_file(self, tmp_path: Path) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=False,
) as sink:
sink.add_json({"version": "x"}, "metadata.json")
assert json.loads((target / "metadata.json").read_text()) == {"version": "x"}
def test_stream_writes_manifest(self, tmp_path: Path) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=False,
) as sink:
with sink.stream("manifest.json") as handle:
writer: StreamingManifestWriter = StreamingManifestWriter(handle)
writer.write_record({"pk": 1})
writer.close()
assert json.loads((target / "manifest.json").read_text()) == [{"pk": 1}]
def test_add_file_skips_when_size_and_mtime_match(
self,
tmp_path: Path,
source_file: Path,
) -> None:
# Pre-existing target with identical size+mtime but DIFFERENT content:
# if add_file skips (no compare-checksums), the old content survives.
target: Path = tmp_path / "out"
target.mkdir()
existing: Path = target / "originals" / "doc.pdf"
existing.parent.mkdir(parents=True)
# Same byte length as the source but different content + matching mtime,
# so a size/mtime comparison treats it as unchanged and skips the copy.
existing.write_bytes(b"X" * len(b"PDF-CONTENT"))
stat = source_file.stat()
os.utime(existing, (stat.st_atime, stat.st_mtime))
with DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=False,
) as sink:
sink.add_file(source_file, "originals/doc.pdf", checksum="abc")
assert existing.read_bytes() == b"X" * len(b"PDF-CONTENT") # skipped
def test_add_file_recopies_when_compare_checksums_differ(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
existing: Path = target / "originals" / "doc.pdf"
existing.parent.mkdir(parents=True)
existing.write_bytes(b"X" * len(b"PDF-CONTENT"))
stat = source_file.stat()
os.utime(existing, (stat.st_atime, stat.st_mtime))
with DirectoryExportSink(
target,
compare_checksums=True,
compare_json=False,
delete=False,
) as sink:
# wrong checksum forces recopy despite matching size/mtime
sink.add_file(source_file, "originals/doc.pdf", checksum="not-the-real-sum")
assert existing.read_bytes() == b"PDF-CONTENT" # recopied
def test_delete_prunes_unwritten_snapshot_files(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
stale: Path = target / "stale.pdf"
stale.write_bytes(b"STALE")
with DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=True,
) as sink:
sink.add_file(source_file, "originals/doc.pdf")
assert not stale.exists()
assert (target / "originals" / "doc.pdf").exists()
def test_no_delete_keeps_unwritten_files(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
stale: Path = target / "stale.pdf"
stale.write_bytes(b"STALE")
with DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=False,
) as sink:
sink.add_file(source_file, "originals/doc.pdf")
assert stale.exists()
class TestZipExportSink:
def test_round_trip_files_json_and_stream(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with ZipExportSink(target, "export", delete=False) as sink:
sink.add_file(source_file, "originals/doc.pdf")
sink.add_json({"version": "x"}, "metadata.json")
with sink.stream("manifest.json") as handle:
writer = StreamingManifestWriter(handle)
writer.write_record({"pk": 1})
writer.close()
zip_path: Path = target / "export.zip"
assert zip_path.exists()
assert not (target / "export.zip.tmp").exists()
with zipfile.ZipFile(zip_path) as zf:
names = set(zf.namelist())
assert {"originals/doc.pdf", "metadata.json", "manifest.json"} <= names
assert zf.read("originals/doc.pdf") == b"PDF-CONTENT"
assert json.loads(zf.read("manifest.json")) == [{"pk": 1}]
def test_nested_arcname_emits_directory_marker(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with ZipExportSink(target, "export", delete=False) as sink:
sink.add_file(source_file, "originals/doc.pdf")
with zipfile.ZipFile(target / "export.zip") as zf:
assert "originals/" in zf.namelist()
def test_flat_arcname_has_no_directory_markers(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with ZipExportSink(target, "export", delete=False) as sink:
sink.add_file(source_file, "doc.pdf")
with zipfile.ZipFile(target / "export.zip") as zf:
assert all(not n.endswith("/") for n in zf.namelist())
def test_exception_leaves_no_zip_and_no_tmp(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
with pytest.raises(RuntimeError):
with ZipExportSink(target, "export", delete=False) as sink:
sink.add_file(source_file, "doc.pdf")
raise RuntimeError("boom")
assert not (target / "export.zip").exists()
assert not (target / "export.zip.tmp").exists()
def test_exception_inside_stream_cleans_up_manifest_tmp(
self,
tmp_path: Path,
source_file: Path,
settings: SettingsWrapper,
) -> None:
scratch_dir = tmp_path / "scratch"
settings.SCRATCH_DIR = scratch_dir
target: Path = tmp_path / "out"
target.mkdir()
with pytest.raises(RuntimeError):
with ZipExportSink(target, "export", delete=False) as sink:
sink.add_file(source_file, "doc.pdf")
with sink.stream("manifest.json") as handle:
handle.write("[")
raise RuntimeError("boom")
assert list(scratch_dir.glob("export-manifest-*")) == []
assert not (target / "export.zip").exists()
assert not (target / "export.zip.tmp").exists()
def test_abort_after_manifest_written_cleans_up_pending_tmp(
self,
tmp_path: Path,
settings: SettingsWrapper,
) -> None:
scratch_dir = tmp_path / "scratch"
settings.SCRATCH_DIR = scratch_dir
target: Path = tmp_path / "out"
target.mkdir()
with pytest.raises(RuntimeError):
with ZipExportSink(target, "export", delete=False) as sink:
with sink.stream("manifest.json") as handle:
handle.write("[]")
raise RuntimeError("boom")
assert list(scratch_dir.glob("export-manifest-*")) == []
assert not (target / "export.zip").exists()
def test_delete_wipes_destination_on_success(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
(target / "preexisting.txt").write_text("old")
(target / "olddir").mkdir()
with ZipExportSink(target, "export", delete=True) as sink:
sink.add_file(source_file, "doc.pdf")
assert (target / "export.zip").exists()
assert not (target / "preexisting.txt").exists()
assert not (target / "olddir").exists()
def test_abort_with_delete_does_not_wipe_destination(
self,
tmp_path: Path,
source_file: Path,
) -> None:
target: Path = tmp_path / "out"
target.mkdir()
(target / "preexisting.txt").write_text("old")
with pytest.raises(RuntimeError):
with ZipExportSink(target, "export", delete=True) as sink:
sink.add_file(source_file, "doc.pdf")
raise RuntimeError("boom")
assert (target / "preexisting.txt").exists()
assert not (target / "export.zip").exists()
class TestStreamContract:
@pytest.fixture(params=["dir", "zip"])
def sink(self, request: pytest.FixtureRequest, tmp_path: Path) -> ExportSink:
target: Path = tmp_path / "out"
target.mkdir()
if request.param == "dir":
return DirectoryExportSink(
target,
compare_checksums=False,
compare_json=False,
delete=False,
)
return ZipExportSink(target, "export", delete=False)
def test_second_concurrent_stream_is_rejected(self, sink: ExportSink) -> None:
with sink:
with sink.stream("manifest.json"):
with pytest.raises(RuntimeError, match="already open"):
with sink.stream("other.json"):
pass
+46 -27
View File
@@ -1057,33 +1057,52 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
THEN: THEN:
- The similar documents are returned from the API request - The similar documents are returned from the API request
""" """
# Distinct created/added dates: documents created at the same instant # Distinct created/added/modified dates: documents sharing a timestamp
# share a timestamp term, and more_like_this (which cannot be scoped to # term (down to the second) would be matched on it by more_like_this
# content fields) would then match on it, surfacing unrelated documents. # (which cannot be scoped to content fields), surfacing unrelated
d1 = DocumentFactory( # documents. `modified` is auto_now, so it can't be set via factory
title="invoice", # kwargs like created/added - freeze time per document instead so all
content="the thing i bought at a shop and paid with bank account", # three date fields land on distinct seconds.
created=datetime.date(2018, 1, 1), with time_machine.travel(
added=timezone.make_aware(datetime.datetime(2018, 1, 1)), timezone.make_aware(datetime.datetime(2018, 1, 1)),
) tick=False,
d2 = DocumentFactory( ):
title="bank statement 1", d1 = DocumentFactory(
content="things i paid for in august", title="invoice",
created=datetime.date(2019, 3, 4), content="the thing i bought at a shop and paid with bank account",
added=timezone.make_aware(datetime.datetime(2019, 3, 4)), created=datetime.date(2018, 1, 1),
) added=timezone.make_aware(datetime.datetime(2018, 1, 1)),
d3 = DocumentFactory( )
title="bank statement 3", with time_machine.travel(
content="things i paid for in september", timezone.make_aware(datetime.datetime(2019, 3, 4)),
created=datetime.date(2020, 7, 9), tick=False,
added=timezone.make_aware(datetime.datetime(2020, 7, 9)), ):
) d2 = DocumentFactory(
d4 = DocumentFactory( title="bank statement 1",
title="Quarterly Report", content="things i paid for in august",
content="quarterly revenue profit margin earnings growth", created=datetime.date(2019, 3, 4),
created=datetime.date(2021, 11, 30), added=timezone.make_aware(datetime.datetime(2019, 3, 4)),
added=timezone.make_aware(datetime.datetime(2021, 11, 30)), )
) with time_machine.travel(
timezone.make_aware(datetime.datetime(2020, 7, 9)),
tick=False,
):
d3 = DocumentFactory(
title="bank statement 3",
content="things i paid for in september",
created=datetime.date(2020, 7, 9),
added=timezone.make_aware(datetime.datetime(2020, 7, 9)),
)
with time_machine.travel(
timezone.make_aware(datetime.datetime(2021, 11, 30)),
tick=False,
):
d4 = DocumentFactory(
title="Quarterly Report",
content="quarterly revenue profit margin earnings growth",
created=datetime.date(2021, 11, 30),
added=timezone.make_aware(datetime.datetime(2021, 11, 30)),
)
backend = get_backend() backend = get_backend()
backend.add_or_update(d1) backend.add_or_update(d1)
backend.add_or_update(d2) backend.add_or_update(d2)
@@ -426,7 +426,7 @@ class TestExportImport(
st_mtime_1 = (self.target / "manifest.json").stat().st_mtime st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
with mock.patch( with mock.patch(
"documents.management.commands.document_exporter.copy_file_with_basic_stats", "documents.export.sinks.copy_file_with_basic_stats",
) as m: ) as m:
self._do_export() self._do_export()
m.assert_not_called() m.assert_not_called()
@@ -437,7 +437,7 @@ class TestExportImport(
Path(self.d1.source_path).touch() Path(self.d1.source_path).touch()
with mock.patch( with mock.patch(
"documents.management.commands.document_exporter.copy_file_with_basic_stats", "documents.export.sinks.copy_file_with_basic_stats",
) as m: ) as m:
self._do_export() self._do_export()
self.assertEqual(m.call_count, 1) self.assertEqual(m.call_count, 1)
@@ -464,7 +464,7 @@ class TestExportImport(
self.assertIsFile(self.target / "manifest.json") self.assertIsFile(self.target / "manifest.json")
with mock.patch( with mock.patch(
"documents.management.commands.document_exporter.copy_file_with_basic_stats", "documents.export.sinks.copy_file_with_basic_stats",
) as m: ) as m:
self._do_export() self._do_export()
m.assert_not_called() m.assert_not_called()
@@ -475,7 +475,7 @@ class TestExportImport(
self.d2.save() self.d2.save()
with mock.patch( with mock.patch(
"documents.management.commands.document_exporter.copy_file_with_basic_stats", "documents.export.sinks.copy_file_with_basic_stats",
) as m: ) as m:
self._do_export(compare_checksums=True) self._do_export(compare_checksums=True)
self.assertEqual(m.call_count, 1) self.assertEqual(m.call_count, 1)
@@ -1058,6 +1058,26 @@ class TestExportImport(
self.assertEqual(Document.objects.all().count(), 4) self.assertEqual(Document.objects.all().count(), 4)
def test_zip_with_compare_flags_raises(self) -> None:
"""
GIVEN:
- A request to export to a zip file
WHEN:
- --compare-checksums or --compare-json is also passed
THEN:
- A CommandError is raised (the flags are no-ops in zip mode)
"""
for flag in ("--compare-checksums", "--compare-json"):
with self.subTest(flag=flag):
with self.assertRaises(CommandError):
call_command(
"document_exporter",
self.target,
"--zip",
flag,
skip_checks=True,
)
@pytest.mark.management @pytest.mark.management
class TestCryptExportImport( class TestCryptExportImport(
@@ -12,9 +12,22 @@ from django.test import override_settings
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from rest_framework.test import APIClient from rest_framework.test import APIClient
from documents.matching import match_correspondents
from documents.matching import match_document_types
from documents.matching import match_storage_paths
from documents.matching import match_tags
from documents.models import Correspondent
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.permissions import permitted_document_ids from documents.permissions import permitted_document_ids
from documents.permissions import permitted_object_ids
from documents.serialisers import _get_viewable_duplicates from documents.serialisers import _get_viewable_duplicates
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory from documents.tests.factories import DocumentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import StoragePathFactory
from documents.tests.factories import TagFactory
def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden): def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden):
@@ -431,3 +444,320 @@ class TestTrashRestorePermissionBoundary:
format="json", format="json",
) )
assert response.status_code == HTTPStatus.OK assert response.status_code == HTTPStatus.OK
@pytest.mark.django_db
class TestTrashViewExcludesExplicitlyGrantedDocuments:
"""
Regression test pinning TrashView's use of
``_TrashPermittedObjectsFilter`` (``include_granted = False``). If that
flag were ever flipped to the default ``True``, or the subclass removed
in favor of the base ``PermittedObjectsFilter``, a trashed document
would leak into ``/api/trash/`` results for any user holding an
explicit guardian grant on it, even though they are neither the owner
nor a superuser.
"""
def test_explicit_grant_does_not_leak_trashed_document(self, rest_api_client):
owner = User.objects.create_user(username="trash_owner")
grantee = User.objects.create_user(username="trash_grantee")
doc = DocumentFactory(owner=owner)
doc.delete() # soft delete
assign_perm("view_document", grantee, doc)
rest_api_client.force_authenticate(user=grantee)
response = rest_api_client.get("/api/trash/")
assert response.status_code == HTTPStatus.OK
result_ids = {result["id"] for result in response.data["results"]}
assert doc.pk not in result_ids
@pytest.mark.django_db
@pytest.mark.parametrize(
("model", "factory", "perm"),
[
(Tag, TagFactory, "view_tag"),
(Correspondent, CorrespondentFactory, "view_correspondent"),
(DocumentType, DocumentTypeFactory, "view_documenttype"),
(StoragePath, StoragePathFactory, "view_storagepath"),
],
)
class TestPermittedObjectIdsGenericModels:
def test_owner_sees_own_object(self, model, factory, perm):
owner = User.objects.create_user(username=f"owner_{model.__name__}")
stranger = User.objects.create_user(username=f"stranger_{model.__name__}")
owned = factory(owner=owner)
strangers = factory(owner=stranger)
assert_visible_document_ids(
permitted_object_ids(owner, model, perm),
expected_visible=[owned.pk],
expected_hidden=[strangers.pk],
)
def test_unowned_object_visible_to_everyone(self, model, factory, perm):
user = User.objects.create_user(username=f"user_{model.__name__}")
unowned = factory(owner=None)
assert_visible_document_ids(
permitted_object_ids(user, model, perm),
expected_visible=[unowned.pk],
expected_hidden=[],
)
def test_explicit_permission_grants_visibility(self, model, factory, perm):
owner = User.objects.create_user(username=f"owner2_{model.__name__}")
grantee = User.objects.create_user(username=f"grantee_{model.__name__}")
stranger = User.objects.create_user(username=f"stranger2_{model.__name__}")
shared = factory(owner=owner)
not_shared = factory(owner=owner)
assign_perm(perm, grantee, shared)
assert_visible_document_ids(
permitted_object_ids(grantee, model, perm),
expected_visible=[shared.pk],
expected_hidden=[not_shared.pk],
)
assert_visible_document_ids(
permitted_object_ids(stranger, model, perm),
expected_visible=[],
expected_hidden=[shared.pk, not_shared.pk],
)
def test_group_permission_grants_visibility_to_members_only(
self,
model,
factory,
perm,
):
owner = User.objects.create_user(username=f"owner3_{model.__name__}")
member = User.objects.create_user(username=f"member_{model.__name__}")
non_member = User.objects.create_user(username=f"nonmember_{model.__name__}")
group = Group.objects.create(name=f"group_{model.__name__}")
member.groups.add(group)
shared = factory(owner=owner)
assign_perm(perm, group, shared)
assert_visible_document_ids(
permitted_object_ids(member, model, perm),
expected_visible=[shared.pk],
expected_hidden=[],
)
assert_visible_document_ids(
permitted_object_ids(non_member, model, perm),
expected_visible=[],
expected_hidden=[shared.pk],
)
def test_superuser_sees_everything(self, model, factory, perm):
superuser = User.objects.create_superuser(username=f"root_{model.__name__}")
owner = User.objects.create_user(username=f"owner4_{model.__name__}")
obj = factory(owner=owner)
assert_visible_document_ids(
permitted_object_ids(superuser, model, perm),
expected_visible=[obj.pk],
expected_hidden=[],
)
@pytest.mark.django_db
class TestMatchingRespectsObjectPermissions:
def test_match_tags_only_considers_tags_visible_to_user(self):
owner = User.objects.create_user(username="tag_owner")
classifying_user = User.objects.create_user(username="classifier_user")
visible_tag = TagFactory(
owner=owner,
match="invoice",
matching_algorithm=Tag.MATCH_LITERAL,
)
hidden_tag = TagFactory(
owner=owner,
match="invoice",
matching_algorithm=Tag.MATCH_LITERAL,
)
assign_perm("view_tag", classifying_user, visible_tag)
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_tags(doc, classifier=None, user=classifying_user)
matched_ids = {t.pk for t in matched}
assert visible_tag.pk in matched_ids
assert hidden_tag.pk not in matched_ids
def test_match_correspondents_only_considers_correspondents_visible_to_user(self):
owner = User.objects.create_user(username="correspondent_owner")
classifying_user = User.objects.create_user(username="classifier_user2")
visible_correspondent = CorrespondentFactory(
owner=owner,
match="invoice",
matching_algorithm=Correspondent.MATCH_LITERAL,
)
hidden_correspondent = CorrespondentFactory(
owner=owner,
match="invoice",
matching_algorithm=Correspondent.MATCH_LITERAL,
)
assign_perm("view_correspondent", classifying_user, visible_correspondent)
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_correspondents(doc, classifier=None, user=classifying_user)
matched_ids = {c.pk for c in matched}
assert visible_correspondent.pk in matched_ids
assert hidden_correspondent.pk not in matched_ids
def test_match_document_types_only_considers_document_types_visible_to_user(self):
owner = User.objects.create_user(username="document_type_owner")
classifying_user = User.objects.create_user(username="classifier_user3")
visible_document_type = DocumentTypeFactory(
owner=owner,
match="invoice",
matching_algorithm=DocumentType.MATCH_LITERAL,
)
hidden_document_type = DocumentTypeFactory(
owner=owner,
match="invoice",
matching_algorithm=DocumentType.MATCH_LITERAL,
)
assign_perm("view_documenttype", classifying_user, visible_document_type)
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_document_types(doc, classifier=None, user=classifying_user)
matched_ids = {dt.pk for dt in matched}
assert visible_document_type.pk in matched_ids
assert hidden_document_type.pk not in matched_ids
def test_match_storage_paths_only_considers_storage_paths_visible_to_user(self):
owner = User.objects.create_user(username="storage_path_owner")
classifying_user = User.objects.create_user(username="classifier_user4")
visible_storage_path = StoragePathFactory(
owner=owner,
match="invoice",
matching_algorithm=StoragePath.MATCH_LITERAL,
)
hidden_storage_path = StoragePathFactory(
owner=owner,
match="invoice",
matching_algorithm=StoragePath.MATCH_LITERAL,
)
assign_perm("view_storagepath", classifying_user, visible_storage_path)
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_storage_paths(doc, classifier=None, user=classifying_user)
matched_ids = {sp.pk for sp in matched}
assert visible_storage_path.pk in matched_ids
assert hidden_storage_path.pk not in matched_ids
@pytest.mark.django_db
class TestBulkEditObjectsApplyToAllPermissionBoundary:
def test_apply_to_all_tags_excludes_unpermitted_tag(self, rest_api_client):
owner = User.objects.create_user(username="tags_owner")
requester = User.objects.create_user(username="tags_requester")
# grant the global change_tag permission so the object-level
# filtering (not the global has_perm check) is what's under test
requester.user_permissions.add(
Permission.objects.get(codename="change_tag"),
)
rest_api_client.force_authenticate(user=requester)
visible = TagFactory(owner=owner)
hidden = TagFactory(owner=owner)
assign_perm("view_tag", requester, visible)
assign_perm("change_tag", requester, visible)
response = rest_api_client.post(
"/api/bulk_edit_objects/",
{
"object_type": "tags",
"operation": "set_permissions",
"all": True,
"filters": {},
"owner": requester.pk,
},
format="json",
)
assert response.status_code == HTTPStatus.OK
# The apply_to_all dispatch must resolve permitted objects up front:
# the visible tag (object-level change_tag granted) gets its owner
# reassigned, while the hidden tag (no object-level grant) is
# excluded entirely and keeps its original owner.
visible.refresh_from_db()
hidden.refresh_from_db()
assert visible.owner == requester
assert hidden.owner == owner
@pytest.mark.django_db
class TestBulkEditObjectsTagDescendantPartialPermission:
def test_apply_to_all_descendant_expansion_respects_per_object_permissions(
self,
rest_api_client,
):
"""
GIVEN:
- A tag hierarchy (parent -> permitted_child, unpermitted_child)
- A non-superuser requester with object-level change_tag granted
on the parent and on only ONE of the two children
WHEN:
- bulk_edit_objects is called with all=True and a filter that
matches only the root (parent) tag, engaging the
tag-descendant-expansion logic in BulkEditObjectsView.post
THEN:
- The descendant expansion only pulls in descendants the
requester actually has permission on: the permitted child's
owner is reassigned alongside the parent's, while the
unpermitted child keeps its original owner. This pins that the
expansion checks per-object permissions (editable_ids), not
merely "is a descendant of a filter match".
NOTE: this uses ``set_permissions`` (owner reassignment) rather than
``delete`` as the operation, because Tag.tn_parent (django-treenode)
cascades deletes to descendants at the database/ORM level regardless
of which tags the view resolved into ``objs`` -- a delete-based test
would pass/fail based on FK cascade behavior, not on whether the
descendant-expansion logic itself respected per-object permissions.
"""
owner = User.objects.create_user(username="tag_hierarchy_owner")
requester = User.objects.create_user(username="tag_hierarchy_requester")
# global change_tag permission so the has_perm() gate passes and the
# object-level permitted_object_ids filtering is what's under test
requester.user_permissions.add(
Permission.objects.get(codename="change_tag"),
)
rest_api_client.force_authenticate(user=requester)
parent = TagFactory(owner=owner, name="parent-tag")
permitted_child = TagFactory(
owner=owner,
name="permitted-child-tag",
tn_parent=parent,
)
unpermitted_child = TagFactory(
owner=owner,
name="unpermitted-child-tag",
tn_parent=parent,
)
assign_perm("change_tag", requester, parent)
assign_perm("change_tag", requester, permitted_child)
# unpermitted_child is intentionally NOT granted change_tag
response = rest_api_client.post(
"/api/bulk_edit_objects/",
{
"object_type": "tags",
"operation": "set_permissions",
"all": True,
"filters": {"is_root": True},
"owner": requester.pk,
},
format="json",
)
assert response.status_code == HTTPStatus.OK
parent.refresh_from_db()
permitted_child.refresh_from_db()
unpermitted_child.refresh_from_db()
assert parent.owner == requester
assert permitted_child.owner == requester
assert unpermitted_child.owner == owner
@@ -0,0 +1,70 @@
import pytest
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm
from rest_framework.test import APIRequestFactory
from documents.filters import PermittedObjectsFilter
from documents.models import Tag
from documents.tests.factories import TagFactory
class _DummyView:
queryset = Tag.objects.all()
@pytest.mark.django_db
class TestPermittedObjectsFilter:
def test_superuser_bypasses_filtering_entirely(self):
superuser = User.objects.create_superuser(username="root")
owner = User.objects.create_user(username="owner")
TagFactory(owner=owner)
request = APIRequestFactory().get("/")
request.user = superuser
result = PermittedObjectsFilter().filter_queryset(
request,
Tag.objects.all(),
_DummyView(),
)
assert result.count() == Tag.objects.count()
def test_non_superuser_sees_only_owned_unowned_and_granted(self):
owner = User.objects.create_user(username="owner")
grantee = User.objects.create_user(username="grantee")
owned = TagFactory(owner=grantee)
unowned = TagFactory(owner=None)
granted = TagFactory(owner=owner)
hidden = TagFactory(owner=owner)
assign_perm("view_tag", grantee, granted)
request = APIRequestFactory().get("/")
request.user = grantee
result = PermittedObjectsFilter().filter_queryset(
request,
Tag.objects.all(),
_DummyView(),
)
visible_ids = set(result.values_list("id", flat=True))
assert visible_ids == {owned.pk, unowned.pk, granted.pk}
assert hidden.pk not in visible_ids
def test_include_granted_false_excludes_explicitly_shared_objects(self):
owner = User.objects.create_user(username="owner2")
grantee = User.objects.create_user(username="grantee2")
owned = TagFactory(owner=grantee)
granted = TagFactory(owner=owner)
assign_perm("view_tag", grantee, granted)
request = APIRequestFactory().get("/")
request.user = grantee
class _OwnerOnlyFilter(PermittedObjectsFilter):
include_granted = False
result = _OwnerOnlyFilter().filter_queryset(
request,
Tag.objects.all(),
_DummyView(),
)
visible_ids = set(result.values_list("id", flat=True))
assert visible_ids == {owned.pk}
assert granted.pk not in visible_ids
-4
View File
@@ -78,10 +78,6 @@ class TestViews(DirectoriesMixin, TestCase):
response.context_data["styles_css"], response.context_data["styles_css"],
f"frontend/{language_actual}/styles.css", f"frontend/{language_actual}/styles.css",
) )
self.assertEqual(
response.context_data["runtime_js"],
f"frontend/{language_actual}/runtime.js",
)
self.assertEqual( self.assertEqual(
response.context_data["polyfills_js"], response.context_data["polyfills_js"],
f"frontend/{language_actual}/polyfills.js", f"frontend/{language_actual}/polyfills.js",
+22 -19
View File
@@ -133,12 +133,10 @@ from documents.file_handling import format_filename
from documents.filters import CorrespondentFilterSet from documents.filters import CorrespondentFilterSet
from documents.filters import CustomFieldFilterSet from documents.filters import CustomFieldFilterSet
from documents.filters import DocumentFilterSet from documents.filters import DocumentFilterSet
from documents.filters import DocumentPermissionsFilter
from documents.filters import DocumentsOrderingFilter from documents.filters import DocumentsOrderingFilter
from documents.filters import DocumentTypeFilterSet from documents.filters import DocumentTypeFilterSet
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
from documents.filters import ObjectOwnedPermissionsFilter
from documents.filters import PaperlessTaskFilterSet from documents.filters import PaperlessTaskFilterSet
from documents.filters import PermittedObjectsFilter
from documents.filters import ShareLinkBundleFilterSet from documents.filters import ShareLinkBundleFilterSet
from documents.filters import ShareLinkFilterSet from documents.filters import ShareLinkFilterSet
from documents.filters import StoragePathFilterSet from documents.filters import StoragePathFilterSet
@@ -178,6 +176,7 @@ from documents.permissions import has_global_statistics_permission
from documents.permissions import has_perms_owner_aware from documents.permissions import has_perms_owner_aware
from documents.permissions import has_system_status_permission from documents.permissions import has_system_status_permission
from documents.permissions import permitted_document_ids from documents.permissions import permitted_document_ids
from documents.permissions import permitted_object_ids
from documents.permissions import set_permissions_for_object from documents.permissions import set_permissions_for_object
from documents.plugins.date_parsing import get_date_parser from documents.plugins.date_parsing import get_date_parser
from documents.schema import generate_object_with_permissions_schema from documents.schema import generate_object_with_permissions_schema
@@ -348,7 +347,6 @@ class IndexView(TemplateView):
context["username"] = self.request.user.username context["username"] = self.request.user.username
context["full_name"] = self.request.user.get_full_name() context["full_name"] = self.request.user.get_full_name()
context["styles_css"] = f"frontend/{self.get_frontend_language()}/styles.css" context["styles_css"] = f"frontend/{self.get_frontend_language()}/styles.css"
context["runtime_js"] = f"frontend/{self.get_frontend_language()}/runtime.js"
context["polyfills_js"] = ( context["polyfills_js"] = (
f"frontend/{self.get_frontend_language()}/polyfills.js" f"frontend/{self.get_frontend_language()}/polyfills.js"
) )
@@ -551,7 +549,7 @@ class CorrespondentViewSet(
filter_backends = ( filter_backends = (
DjangoFilterBackend, DjangoFilterBackend,
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = CorrespondentFilterSet filterset_class = CorrespondentFilterSet
ordering_fields = ( ordering_fields = (
@@ -592,7 +590,7 @@ class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Tag]):
filter_backends = ( filter_backends = (
DjangoFilterBackend, DjangoFilterBackend,
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = TagFilterSet filterset_class = TagFilterSet
ordering_fields = ("color", "name", "matching_algorithm", "match", "document_count") ordering_fields = ("color", "name", "matching_algorithm", "match", "document_count")
@@ -684,7 +682,7 @@ class DocumentTypeViewSet(
filter_backends = ( filter_backends = (
DjangoFilterBackend, DjangoFilterBackend,
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = DocumentTypeFilterSet filterset_class = DocumentTypeFilterSet
ordering_fields = ("name", "matching_algorithm", "match", "document_count") ordering_fields = ("name", "matching_algorithm", "match", "document_count")
@@ -988,7 +986,7 @@ class DocumentViewSet(
DjangoFilterBackend, DjangoFilterBackend,
SearchFilter, SearchFilter,
DocumentsOrderingFilter, DocumentsOrderingFilter,
DocumentPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = DocumentFilterSet filterset_class = DocumentFilterSet
search_fields = ("title", "correspondent__name", "effective_content") search_fields = ("title", "correspondent__name", "effective_content")
@@ -2674,7 +2672,7 @@ class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet[SavedVie
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = ( filter_backends = (
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
ordering_fields = ("name",) ordering_fields = ("name",)
@@ -3921,7 +3919,7 @@ class StoragePathViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Storag
filter_backends = ( filter_backends = (
DjangoFilterBackend, DjangoFilterBackend,
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = StoragePathFilterSet filterset_class = StoragePathFilterSet
ordering_fields = ("name", "path", "matching_algorithm", "match", "document_count") ordering_fields = ("name", "path", "matching_algorithm", "match", "document_count")
@@ -4452,7 +4450,7 @@ class ShareLinkViewSet(
filter_backends = ( filter_backends = (
DjangoFilterBackend, DjangoFilterBackend,
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = ShareLinkFilterSet filterset_class = ShareLinkFilterSet
ordering_fields = ("created", "expiration", "document") ordering_fields = ("created", "expiration", "document")
@@ -4482,7 +4480,7 @@ class ShareLinkBundleViewSet(PassUserMixin, ModelViewSet[ShareLinkBundle]):
filter_backends = ( filter_backends = (
DjangoFilterBackend, DjangoFilterBackend,
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, PermittedObjectsFilter,
) )
filterset_class = ShareLinkBundleFilterSet filterset_class = ShareLinkBundleFilterSet
ordering_fields = ("created", "expiration", "status") ordering_fields = ("created", "expiration", "status")
@@ -4765,10 +4763,8 @@ class BulkEditObjectsView(PassUserMixin):
"document_types": DocumentTypeFilterSet, "document_types": DocumentTypeFilterSet,
"storage_paths": StoragePathFilterSet, "storage_paths": StoragePathFilterSet,
}[object_type] }[object_type]
user_permitted_objects = get_objects_for_user_owner_aware( user_permitted_objects = object_class.objects.filter(
user, id__in=permitted_object_ids(user, object_class, perm_codename),
perm_codename,
object_class,
) )
objs = filterset_class( objs = filterset_class(
data=filters, data=filters,
@@ -4793,8 +4789,11 @@ class BulkEditObjectsView(PassUserMixin):
if not user.is_superuser: if not user.is_superuser:
perm = f"documents.{perm_codename}" perm = f"documents.{perm_codename}"
has_perms = user.has_perm(perm) and all( has_perms = (
has_perms_owner_aware(user, perm_codename, obj) for obj in objs user.has_perm(perm)
and not objs.exclude(
pk__in=permitted_object_ids(user, object_class, perm_codename),
).exists()
) )
if not has_perms: if not has_perms:
@@ -5295,7 +5294,11 @@ class SystemStatusView(PassUserMixin):
class TrashView(ListModelMixin, PassUserMixin): class TrashView(ListModelMixin, PassUserMixin):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = TrashSerializer serializer_class = TrashSerializer
filter_backends = (ObjectOwnedPermissionsFilter,)
class _TrashPermittedObjectsFilter(PermittedObjectsFilter):
include_granted = False
filter_backends = (_TrashPermittedObjectsFilter,)
pagination_class = StandardPagination pagination_class = StandardPagination
model = Document model = Document
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumente" msgstr "Dokumente"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Waarde moet geldige JSON wees." msgstr "Waarde moet geldige JSON wees."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ongeldige gepasmaakte veldnavraaguitdrukking" msgstr "Ongeldige gepasmaakte veldnavraaguitdrukking"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ongeldige uitdrukking lys. Moet nie leeg wees nie." msgstr "Ongeldige uitdrukking lys. Moet nie leeg wees nie."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ongeldige logiese uitdrukking {op!r}" msgstr "Ongeldige logiese uitdrukking {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "" msgstr ""
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "" msgstr ""
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "" msgstr ""
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ongeldige kleur." msgstr "Ongeldige kleur."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Lêertipe %(type)s word nie ondersteun nie" msgstr "Lêertipe %(type)s word nie ondersteun nie"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ongeldige veranderlike bespeur." msgstr "Ongeldige veranderlike bespeur."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Amharic\n" "Language-Team: Amharic\n"
"Language: am_ET\n" "Language: am_ET\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "መዝገባት" msgstr "መዝገባት"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "የሚሰራው እሴት \"JSON\" መሆን አለበት" msgstr "የሚሰራው እሴት \"JSON\" መሆን አለበት"
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "ልክ ያልሆነ የተወሰነ የቦታ መጠይቅ አገላለጽ" msgstr "ልክ ያልሆነ የተወሰነ የቦታ መጠይቅ አገላለጽ"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "ልክ ያልሆነ የመግለጫ ዝርዝር። ባዶ መሆን የለበትም።" msgstr "ልክ ያልሆነ የመግለጫ ዝርዝር። ባዶ መሆን የለበትም።"
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "ልክ ያልሆነ የሎጂክ ኦፕሬተር {op!r}" msgstr "ልክ ያልሆነ የሎጂክ ኦፕሬተር {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "ከፍተኛው የጥያቄ ሁኔታዎች/መጠን ብዛት አልፏል።" msgstr "ከፍተኛው የጥያቄ ሁኔታዎች/መጠን ብዛት አልፏል።"
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ይሄ ታዐማኒነት ያለው ልማድ አይደለም።" msgstr "{name!r} ይሄ ታዐማኒነት ያለው ልማድ አይደለም።"
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "ጥያቄን አይደግፍም expr {expr!r}." msgstr "ጥያቄን አይደግፍም expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።" msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።"
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "ይህ ልማድ አልተገኘም" msgstr "ይህ ልማድ አልተገኘም"
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "المستندات" msgstr "المستندات"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "يجب أن تكون القيمة JSON." msgstr "يجب أن تكون القيمة JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "تعبير استعلام غير صالح للحقول المخصصة" msgstr "تعبير استعلام غير صالح للحقول المخصصة"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "قائمة عبارة خاطئة." msgstr "قائمة عبارة خاطئة."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "تجاوز الحد الأقصى لعدد شروط الاستعلام." msgstr "تجاوز الحد الأقصى لعدد شروط الاستعلام."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} حقل مخصص غير صالح." msgstr "{name!r} حقل مخصص غير صالح."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} لا يدعم تعبير الاستعلام {expr!r}." msgstr "{data_type} لا يدعم تعبير الاستعلام {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "لم يتم العثور على حقل مخصص" msgstr "لم يتم العثور على حقل مخصص"
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "لون خاطئ." msgstr "لون خاطئ."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "نوع الملف %(type)s غير مدعوم" msgstr "نوع الملف %(type)s غير مدعوم"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "اكتشاف متغير خاطئ." msgstr "اكتشاف متغير خاطئ."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Belarusian\n" "Language-Team: Belarusian\n"
"Language: be_BY\n" "Language: be_BY\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Дакументы" msgstr "Дакументы"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "" msgstr ""
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "" msgstr ""
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "" msgstr ""
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "" msgstr ""
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "" msgstr ""
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "" msgstr ""
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Няправільны колер." msgstr "Няправільны колер."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Тып файла %(type)s не падтрымліваецца" msgstr "Тып файла %(type)s не падтрымліваецца"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Выяўлена няправільная зменная." msgstr "Выяўлена няправільная зменная."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Документи" msgstr "Документи"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Стойността трябва да е валидна JSON." msgstr "Стойността трябва да е валидна JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Невалидна заявка на персонализираното полето" msgstr "Невалидна заявка на персонализираното полето"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Списък с невалиден израз. Не може да е празно." msgstr "Списък с невалиден израз. Не може да е празно."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Невалиден логически оператор {op!r}" msgstr "Невалиден логически оператор {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Надвишен е максимален брой за заявки." msgstr "Надвишен е максимален брой за заявки."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} не е валидно персонализирано поле." msgstr "{name!r} не е валидно персонализирано поле."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} не поддържа заявка expr {expr!r}." msgstr "{data_type} не поддържа заявка expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Надвишена е максималната дълбочина на вмъкване." msgstr "Надвишена е максималната дълбочина на вмъкване."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Персонализирано поле не е намерено" msgstr "Персонализирано поле не е намерено"
@@ -1338,48 +1338,48 @@ msgstr "стартиране на работния процес"
msgid "workflow runs" msgid "workflow runs"
msgstr "стартиране на работните процеси" msgstr "стартиране на работните процеси"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Невалиден цвят." msgstr "Невалиден цвят."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Файловия тип %(type)s не се поддържа" msgstr "Файловия тип %(type)s не се поддържа"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Засечена е невалидна променлива." msgstr "Засечена е невалидна променлива."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Documents " msgstr "Documents "
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Valor ha de ser un JSON valid." msgstr "Valor ha de ser un JSON valid."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Expressió de camp de consulta invàlid" msgstr "Expressió de camp de consulta invàlid"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Expressió de llista invàlida. No ha d'estar buida." msgstr "Expressió de llista invàlida. No ha d'estar buida."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Invàlid operand lògic {op!r}" msgstr "Invàlid operand lògic {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Condicions de consulta excedits." msgstr "Condicions de consulta excedits."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} no és un camp personalitzat vàlid." msgstr "{name!r} no és un camp personalitzat vàlid."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} no suporta expressió de consulta {expr!r}." msgstr "{data_type} no suporta expressió de consulta {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Màxima profunditat anidada excedida." msgstr "Màxima profunditat anidada excedida."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Camp personalitzat no trobat" msgstr "Camp personalitzat no trobat"
@@ -1338,48 +1338,48 @@ msgstr "data del flux"
msgid "workflow runs" msgid "workflow runs"
msgstr "flux corrents" msgstr "flux corrents"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Permisos insuficients." msgstr "Permisos insuficients."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Color Invàlid." msgstr "Color Invàlid."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Tipus arxiu %(type)s no suportat" msgstr "Tipus arxiu %(type)s no suportat"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "ID de camp personalizat ha de ser enter: %(id)s" msgstr "ID de camp personalizat ha de ser enter: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Camp personalitzat amb ID %(id)s no existeix" msgstr "Camp personalitzat amb ID %(id)s no existeix"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Camps personalitzats han de ser una llista d'enters o un objecte que mapegi els identificadors amb els valors." msgstr "Camps personalitzats han de ser una llista d'enters o un objecte que mapegi els identificadors amb els valors."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Alguns camps personalitzats no existeixen o s'han especificat dues vegades." msgstr "Alguns camps personalitzats no existeixen o s'han especificat dues vegades."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Variable detectada invàlida." msgstr "Variable detectada invàlida."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Duplicat d'identificadors de documents no permès." msgstr "Duplicat d'identificadors de documents no permès."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Documents no trobats: %(ids)s" msgstr "Documents no trobats: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "L'esquema d'URI '{parts.scheme}' no està permès. Esquemes permesos: {'
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "No s'ha pogut analitzar l'URI {value}" msgstr "No s'ha pogut analitzar l'URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Invalid more_like_id" msgstr "Invalid more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Configuració AI invàlida." msgstr "Configuració AI invàlida."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Especifica només un dels següents valors: text, title_search, query o more_like_id." msgstr "Especifica només un dels següents valors: text, title_search, query o more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Permisos insuficients per compartir document %(id)s." msgstr "Permisos insuficients per compartir document %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paquet ja s'està processant." msgstr "Paquet ja s'està processant."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "El paquet de link encarà s'està preparant. Prova de nou més tard." msgstr "El paquet de link encarà s'està preparant. Prova de nou més tard."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "El paquet d'enllaç no està disponible." msgstr "El paquet d'enllaç no està disponible."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Language: cs_CZ\n" "Language: cs_CZ\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumenty" msgstr "Dokumenty"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Hodnota musí být platný JSON." msgstr "Hodnota musí být platný JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Neplatný výraz dotazu na vlastní pole" msgstr "Neplatný výraz dotazu na vlastní pole"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Neplatný seznam výrazů. Nesmí být prázdný." msgstr "Neplatný seznam výrazů. Nesmí být prázdný."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Neplatný logický operátor {op!r}" msgstr "Neplatný logický operátor {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Překročen maximální počet podmínek dotazu." msgstr "Překročen maximální počet podmínek dotazu."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} není platné vlastní pole." msgstr "{name!r} není platné vlastní pole."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} nepodporuje výraz dotazu {expr!r}." msgstr "{data_type} nepodporuje výraz dotazu {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Překročena maximální hloubka větvení." msgstr "Překročena maximální hloubka větvení."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Vlastní pole nebylo nalezeno" msgstr "Vlastní pole nebylo nalezeno"
@@ -1338,48 +1338,48 @@ msgstr "spuštění pracovního postupu"
msgid "workflow runs" msgid "workflow runs"
msgstr "spuštění pracovních postupů" msgstr "spuštění pracovních postupů"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Nedostatečná oprávnění." msgstr "Nedostatečná oprávnění."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Neplatná barva." msgstr "Neplatná barva."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Typ souboru %(type)s není podporován" msgstr "Typ souboru %(type)s není podporován"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Vlastní ID pole musí být celé číslo: %(id)s" msgstr "Vlastní ID pole musí být celé číslo: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Vlastní pole s ID %(id)s neexistuje" msgstr "Vlastní pole s ID %(id)s neexistuje"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Vlastní pole musí být seznam celých čísel nebo ID pro mapování objektů na hodnoty." msgstr "Vlastní pole musí být seznam celých čísel nebo ID pro mapování objektů na hodnoty."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Některá vlastní pole neexistují nebo byla zadána dvakrát." msgstr "Některá vlastní pole neexistují nebo byla zadána dvakrát."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Zjištěna neplatná proměnná." msgstr "Zjištěna neplatná proměnná."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1636,36 +1636,36 @@ msgstr "URI schéma '{parts.scheme}' není povoleno. Povolená schémata: {',\n"
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "Nelze zpracovat URI {value}" msgstr "Nelze zpracovat URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Nedostatečná oprávnění ke sdílení dokumentu %(id)s." msgstr "Nedostatečná oprávnění ke sdílení dokumentu %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Language: da_DK\n" "Language: da_DK\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumenter" msgstr "Dokumenter"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Værdien skal være gyldig JSON." msgstr "Værdien skal være gyldig JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ugyldigt tilpasset feltforespørgselsudtryk" msgstr "Ugyldigt tilpasset feltforespørgselsudtryk"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ugyldig udtryksliste. Må ikke være tom." msgstr "Ugyldig udtryksliste. Må ikke være tom."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ugyldig logisk operatør {op!r}" msgstr "Ugyldig logisk operatør {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maksimalt antal forespørgselsbetingelser overskredet." msgstr "Maksimalt antal forespørgselsbetingelser overskredet."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} er ikke et gyldigt tilpasset felt." msgstr "{name!r} er ikke et gyldigt tilpasset felt."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} understøtter ikke forespørgsel expr {expr!r}." msgstr "{data_type} understøtter ikke forespørgsel expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maksimal indlejringsdybde overskredet." msgstr "Maksimal indlejringsdybde overskredet."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Tilpasset felt ikke fundet" msgstr "Tilpasset felt ikke fundet"
@@ -1338,48 +1338,48 @@ msgstr "workflow-kørsel"
msgid "workflow runs" msgid "workflow runs"
msgstr "workflow-kørsler" msgstr "workflow-kørsler"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ugyldig farve." msgstr "Ugyldig farve."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Filtype %(type)s understøttes ikke" msgstr "Filtype %(type)s understøttes ikke"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ugyldig variabel fundet." msgstr "Ugyldig variabel fundet."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German, Switzerland\n" "Language-Team: German, Switzerland\n"
"Language: de_CH\n" "Language: de_CH\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumente" msgstr "Dokumente"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Wert muss gültiges JSON sein." msgstr "Wert muss gültiges JSON sein."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ungültiger benutzerdefinierter Feldabfrageausdruck" msgstr "Ungültiger benutzerdefinierter Feldabfrageausdruck"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein." msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ungültiger logischer Operator {op!r}" msgstr "Ungültiger logischer Operator {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maximale Anzahl an Abfragebedingungen überschritten." msgstr "Maximale Anzahl an Abfragebedingungen überschritten."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ist kein gültiges Zusatzfeld." msgstr "{name!r} ist kein gültiges Zusatzfeld."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht." msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maximale Verschachtelungstiefe überschritten." msgstr "Maximale Verschachtelungstiefe überschritten."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Benutzerdefiniertes Feld nicht gefunden" msgstr "Benutzerdefiniertes Feld nicht gefunden"
@@ -1338,48 +1338,48 @@ msgstr "Arbeitsablauf-Ausführung"
msgid "workflow runs" msgid "workflow runs"
msgstr "Arbeitsablauf wird ausgeführt" msgstr "Arbeitsablauf wird ausgeführt"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen." msgstr "Unzureichende Berechtigungen."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ungültige Farbe." msgstr "Ungültige Farbe."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Dateityp %(type)s nicht unterstützt" msgstr "Dateityp %(type)s nicht unterstützt"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Feld-ID eines benutzerdefinierten Felds muss eine Ganzzahl sein: %(id)s" msgstr "Feld-ID eines benutzerdefinierten Felds muss eine Ganzzahl sein: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Benutzerdefiniertes Feld mit ID %(id)s existiert nicht" msgstr "Benutzerdefiniertes Feld mit ID %(id)s existiert nicht"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Benutzerdefinierte Felder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein." msgstr "Benutzerdefinierte Felder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Einige benutzerdefinierte Felder existieren nicht oder wurden zweimal angegeben." msgstr "Einige benutzerdefinierte Felder existieren nicht oder wurden zweimal angegeben."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ungültige Variable erkannt." msgstr "Ungültige Variable erkannt."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt." msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumente nicht gefunden: %(ids)s" msgstr "Dokumente nicht gefunden: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "URI-Schema „{parts.scheme}“ ist nicht erlaubt. Erlaubte Schemata: {'
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "URI {value} kann nicht gelesen werden" msgstr "URI {value} kann nicht gelesen werden"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Ungültige more_like_id" msgstr "Ungültige more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Ungültige KI-Konfiguration." msgstr "Ungültige KI-Konfiguration."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an." msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen." msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet." msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut." msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar." msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumente" msgstr "Dokumente"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Wert muss gültiges JSON sein." msgstr "Wert muss gültiges JSON sein."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ungültiger Zusatzfeld-Abfrageausdruck" msgstr "Ungültiger Zusatzfeld-Abfrageausdruck"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein." msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ungültiger logischer Operator {op!r}" msgstr "Ungültiger logischer Operator {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maximale Anzahl an Abfragebedingungen überschritten." msgstr "Maximale Anzahl an Abfragebedingungen überschritten."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ist kein gültiges Zusatzfeld." msgstr "{name!r} ist kein gültiges Zusatzfeld."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht." msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maximale Verschachtelungstiefe überschritten." msgstr "Maximale Verschachtelungstiefe überschritten."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Zusatzfeld nicht gefunden" msgstr "Zusatzfeld nicht gefunden"
@@ -1338,48 +1338,48 @@ msgstr "Arbeitsablauf-Ausführung"
msgid "workflow runs" msgid "workflow runs"
msgstr "Arbeitsablauf wird ausgeführt" msgstr "Arbeitsablauf wird ausgeführt"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen." msgstr "Unzureichende Berechtigungen."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ungültige Farbe." msgstr "Ungültige Farbe."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Dateityp %(type)s nicht unterstützt" msgstr "Dateityp %(type)s nicht unterstützt"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Zusatzfeld-ID muss eine Ganzzahl sein: %(id)s" msgstr "Zusatzfeld-ID muss eine Ganzzahl sein: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Zusatzfeld mit ID %(id)s existiert nicht" msgstr "Zusatzfeld mit ID %(id)s existiert nicht"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Zusatzfelder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein." msgstr "Zusatzfelder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Einige Zusatzfelder existieren nicht oder wurden zweimal angegeben." msgstr "Einige Zusatzfelder existieren nicht oder wurden zweimal angegeben."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ungültige Variable erkannt." msgstr "Ungültige Variable erkannt."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt." msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumente nicht gefunden: %(ids)s" msgstr "Dokumente nicht gefunden: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "URI-Schema „{parts.scheme}“ ist nicht erlaubt. Erlaubte Schemata: {'
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "URI {value} kann nicht gelesen werden" msgstr "URI {value} kann nicht gelesen werden"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Ungültige more_like_id" msgstr "Ungültige more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Ungültige KI-Konfiguration." msgstr "Ungültige KI-Konfiguration."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "Zeitüberschreitung bei der KI-Backendanfrage." msgstr "Zeitüberschreitung bei der KI-Backendanfrage."
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an." msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen." msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet." msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut." msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar." msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Greek\n" "Language-Team: Greek\n"
"Language: el_GR\n" "Language: el_GR\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Έγγραφα" msgstr "Έγγραφα"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Η τιμή πρέπει να είναι σε έγκυρη μορφή JSON." msgstr "Η τιμή πρέπει να είναι σε έγκυρη μορφή JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Μη έγκυρη έκφραση προσαρμοσμένου ερωτήματος πεδίου" msgstr "Μη έγκυρη έκφραση προσαρμοσμένου ερωτήματος πεδίου"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Μη έγκυρη λίστα έκφρασης. Πρέπει να είναι μη κενή." msgstr "Μη έγκυρη λίστα έκφρασης. Πρέπει να είναι μη κενή."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Μη έγκυρος λογικός τελεστής {op!r}" msgstr "Μη έγκυρος λογικός τελεστής {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Υπέρβαση μέγιστου αριθμού συνθηκών ερωτήματος." msgstr "Υπέρβαση μέγιστου αριθμού συνθηκών ερωτήματος."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "Το προσαρμοσμένο πεδίο {name!r} δεν είναι ένα έγκυρο." msgstr "Το προσαρμοσμένο πεδίο {name!r} δεν είναι ένα έγκυρο."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "Το {data_type} δεν υποστηρίζει το ερώτημα expr {expr!r}s." msgstr "Το {data_type} δεν υποστηρίζει το ερώτημα expr {expr!r}s."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Υπέρβαση μέγιστου βάθους εμφώλευσης." msgstr "Υπέρβαση μέγιστου βάθους εμφώλευσης."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Το προσαρμοσμένο πεδίο δε βρέθηκε" msgstr "Το προσαρμοσμένο πεδίο δε βρέθηκε"
@@ -1338,48 +1338,48 @@ msgstr "εκτέλεση ροής εργασίας"
msgid "workflow runs" msgid "workflow runs"
msgstr "εκτελέσεις ροής εργασίας" msgstr "εκτελέσεις ροής εργασίας"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Άκυρο χρώμα." msgstr "Άκυρο χρώμα."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Ο τύπος αρχείου %(type)s δεν υποστηρίζεται" msgstr "Ο τύπος αρχείου %(type)s δεν υποστηρίζεται"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Εντοπίστηκε μη έγκυρη μεταβλητή." msgstr "Εντοπίστηκε μη έγκυρη μεταβλητή."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+20 -20
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-05 14:50+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "" msgstr ""
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "" msgstr ""
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "" msgstr ""
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "" msgstr ""
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "" msgstr ""
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "" msgstr ""
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:756 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1098 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "" msgstr ""
@@ -1352,7 +1352,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:521 documents/serialisers.py:873 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2767 documents/views.py:300 documents/views.py:2557 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1393,7 +1393,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2853 documents/views.py:4511 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1661,36 +1661,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:293 documents/views.py:2554 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1568 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1577 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2379 documents/views.py:2700 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4524 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4570 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4631 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4641 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es_ES\n" "Language: es_ES\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Documentos" msgstr "Documentos"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "El valor debe ser un JSON válido." msgstr "El valor debe ser un JSON válido."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Expresión de consulta de campo personalizado no válida" msgstr "Expresión de consulta de campo personalizado no válida"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Lista de expresiones no válida. No debe estar vacía." msgstr "Lista de expresiones no válida. No debe estar vacía."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Operador lógico inválido {op!r}" msgstr "Operador lógico inválido {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Se ha superado el número máximo de condiciones de consulta." msgstr "Se ha superado el número máximo de condiciones de consulta."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{nombre!r} no es un campo personalizado válido." msgstr "{nombre!r} no es un campo personalizado válido."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} no admite la consulta expr {expr!r}." msgstr "{data_type} no admite la consulta expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Profundidad máxima de nidificación superada." msgstr "Profundidad máxima de nidificación superada."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Campo personalizado no encontrado" msgstr "Campo personalizado no encontrado"
@@ -1338,48 +1338,48 @@ msgstr "ejecución del flujo de trabajo"
msgid "workflow runs" msgid "workflow runs"
msgstr "ejecuciones de flujo de trabajo" msgstr "ejecuciones de flujo de trabajo"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Permisos insuficientes." msgstr "Permisos insuficientes."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Color inválido." msgstr "Color inválido."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Tipo de fichero %(type)s no suportado" msgstr "Tipo de fichero %(type)s no suportado"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "El id del campo personalizado debe ser un entero: %(id)s" msgstr "El id del campo personalizado debe ser un entero: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "El campo personalizado con identificador %(id)s no existe" msgstr "El campo personalizado con identificador %(id)s no existe"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Los campos personalizados deben ser una lista de enteros o un identificador de mapeo de objetos a valores." msgstr "Los campos personalizados deben ser una lista de enteros o un identificador de mapeo de objetos a valores."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Algunos campos personalizados no existen o fueron especificados dos veces." msgstr "Algunos campos personalizados no existen o fueron especificados dos veces."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Variable inválida." msgstr "Variable inválida."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "No se permiten identificadores de documento duplicados." msgstr "No se permiten identificadores de documento duplicados."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Documentos no encontrados: %(ids)s" msgstr "Documentos no encontrados: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "El esquema URI '{parts.scheme}' no está permitido. Esquemas permitidos:
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "No se puede analizar la URI {value}" msgstr "No se puede analizar la URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Configuración de IA inválida." msgstr "Configuración de IA inválida."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Especifique solo uno entre text, title_search, query, o more_like_id." msgstr "Especifique solo uno entre text, title_search, query, o more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Permisos insuficientes para compartir el documento %(id)s." msgstr "Permisos insuficientes para compartir el documento %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "El paquete ya está siendo procesado." msgstr "El paquete ya está siendo procesado."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "El paquete de enlace compartido aún está siendo preparado. Por favor, inténtalo de nuevo más tarde." msgstr "El paquete de enlace compartido aún está siendo preparado. Por favor, inténtalo de nuevo más tarde."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "El paquete de enlace compartido no está disponible." msgstr "El paquete de enlace compartido no está disponible."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Estonian\n" "Language-Team: Estonian\n"
"Language: et_EE\n" "Language: et_EE\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumendid" msgstr "Dokumendid"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Väärtus peab olema lubatav JSON." msgstr "Väärtus peab olema lubatav JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Vigane kohandatud välja päringu avaldis" msgstr "Vigane kohandatud välja päringu avaldis"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Vigane avaldiste loend. Peab olema mittetühi." msgstr "Vigane avaldiste loend. Peab olema mittetühi."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Vigane loogikaoperaator {op!r}" msgstr "Vigane loogikaoperaator {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Päringutingimuste suurim hulk on ületatud." msgstr "Päringutingimuste suurim hulk on ületatud."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ei ole lubatud kohandatud väli." msgstr "{name!r} ei ole lubatud kohandatud väli."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} ei toeta päringu avaldist {expr!r}." msgstr "{data_type} ei toeta päringu avaldist {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Suurim pesastamis sügavus ületatud." msgstr "Suurim pesastamis sügavus ületatud."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Kohandatud välja ei leitud" msgstr "Kohandatud välja ei leitud"
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Language: fa_IR\n" "Language: fa_IR\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "اسناد و مدارک" msgstr "اسناد و مدارک"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "مقدار باید JSON معتبر باشد." msgstr "مقدار باید JSON معتبر باشد."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Invalid custom field query expression" msgstr "Invalid custom field query expression"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "لیست عبارت‌ها نامعتبر است. نباید خالی باشد." msgstr "لیست عبارت‌ها نامعتبر است. نباید خالی باشد."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "حداکثر تعداد شرایط پرس و جو از آن فراتر رفته است." msgstr "حداکثر تعداد شرایط پرس و جو از آن فراتر رفته است."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{نام! R} یک زمینه سفارشی معتبر نیست." msgstr "{نام! R} یک زمینه سفارشی معتبر نیست."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "حداکثر عمق تودرتویی بیش از حد مجاز است." msgstr "حداکثر عمق تودرتویی بیش از حد مجاز است."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "زمینه سفارشی یافت نشد" msgstr "زمینه سفارشی یافت نشد"
@@ -1338,48 +1338,48 @@ msgstr "گردش کار"
msgid "workflow runs" msgid "workflow runs"
msgstr "گردش کار اجرا می شود" msgstr "گردش کار اجرا می شود"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "رنگ نامعتبر" msgstr "رنگ نامعتبر"
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "متغیر نامعتبر شناسایی شده است." msgstr "متغیر نامعتبر شناسایی شده است."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Finnish\n" "Language-Team: Finnish\n"
"Language: fi_FI\n" "Language: fi_FI\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Asiakirjat" msgstr "Asiakirjat"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Arvon on oltava kelvollista JSON:ia." msgstr "Arvon on oltava kelvollista JSON:ia."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "" msgstr ""
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "" msgstr ""
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "" msgstr ""
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "" msgstr ""
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "" msgstr ""
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Virheellinen väri." msgstr "Virheellinen väri."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Tiedostotyyppiä %(type)s ei tueta" msgstr "Tiedostotyyppiä %(type)s ei tueta"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Virheellinen muuttuja havaittu." msgstr "Virheellinen muuttuja havaittu."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Documents" msgstr "Documents"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "La valeur doit être un JSON valide." msgstr "La valeur doit être un JSON valide."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Requête de champ personnalisé invalide" msgstr "Requête de champ personnalisé invalide"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Liste d'expressions invalide. Doit être non vide." msgstr "Liste d'expressions invalide. Doit être non vide."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Opérateur logique {op!r} invalide" msgstr "Opérateur logique {op!r} invalide"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Nombre maximum de conditions dans la requête dépassé." msgstr "Nombre maximum de conditions dans la requête dépassé."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} n'est pas un champ personnalisé valide." msgstr "{name!r} n'est pas un champ personnalisé valide."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} ne supporte pas l'expression {expr!r}." msgstr "{data_type} ne supporte pas l'expression {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Profondeur de récursion maximale dépassée." msgstr "Profondeur de récursion maximale dépassée."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Champ personnalisé non trouvé" msgstr "Champ personnalisé non trouvé"
@@ -1338,48 +1338,48 @@ msgstr "exécution du workflow"
msgid "workflow runs" msgid "workflow runs"
msgstr "le flux de travail s'exécute" msgstr "le flux de travail s'exécute"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Droits insuffisants." msgstr "Droits insuffisants."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Couleur incorrecte." msgstr "Couleur incorrecte."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Type de fichier %(type)s non pris en charge" msgstr "Type de fichier %(type)s non pris en charge"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "L'id du champ personnalisé doit être un entier : %(id)s" msgstr "L'id du champ personnalisé doit être un entier : %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Le champ personnalisé avec l'id %(id)s n'existe pas" msgstr "Le champ personnalisé avec l'id %(id)s n'existe pas"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Les champs personnalisés doivent être une liste d'entiers ou un mappage d'identifiants à des valeurs." msgstr "Les champs personnalisés doivent être une liste d'entiers ou un mappage d'identifiants à des valeurs."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Certains champs personnalisés n'existent pas ou ont été spécifiés deux fois." msgstr "Certains champs personnalisés n'existent pas ou ont été spécifiés deux fois."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Variable invalide détectée." msgstr "Variable invalide détectée."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Les identificateurs de document en double ne sont pas autorisés." msgstr "Les identificateurs de document en double ne sont pas autorisés."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Documents introuvables : %(ids)s" msgstr "Documents introuvables : %(ids)s"
@@ -1634,36 +1634,36 @@ msgstr "Le schéma d'URI « {parts.scheme} » n'est pas autorisé. Schémas aut
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "Impossible d'analyser l'URI {value}" msgstr "Impossible d'analyser l'URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "More_like_id invalide" msgstr "More_like_id invalide"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Configuration IA invalide." msgstr "Configuration IA invalide."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "La requête d'arrière-plan IA a expiré." msgstr "La requête d'arrière-plan IA a expiré."
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Spécifiez seulement un texte, titre, recherche ou more_like_id." msgstr "Spécifiez seulement un texte, titre, recherche ou more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Droits d'accès insuffisant pour partager %(id)s document." msgstr "Droits d'accès insuffisant pour partager %(id)s document."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Le paquet est déjà en cours de traitement." msgstr "Le paquet est déjà en cours de traitement."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Le lot de liens de partage est en cours de préparation. Veuillez réessayer plus tard." msgstr "Le lot de liens de partage est en cours de préparation. Veuillez réessayer plus tard."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Le lot de liens de partage n'est pas disponible." msgstr "Le lot de liens de partage n'est pas disponible."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hebrew\n" "Language-Team: Hebrew\n"
"Language: he_IL\n" "Language: he_IL\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "מסמכים" msgstr "מסמכים"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "ערך חייב להיות JSON תקין." msgstr "ערך חייב להיות JSON תקין."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "ביטוי שאילתה לא חוקי של שדה מותאם אישית" msgstr "ביטוי שאילתה לא חוקי של שדה מותאם אישית"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "רשימת ביטויים לא חוקית. חייב לכלול ערך." msgstr "רשימת ביטויים לא חוקית. חייב לכלול ערך."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "סימן פעולה לוגית לא חוקי {op!r}" msgstr "סימן פעולה לוגית לא חוקי {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "חריגה ממספר תנאי השאילתה המרבי." msgstr "חריגה ממספר תנאי השאילתה המרבי."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} הוא לא שדה מותאם אישית חוקי." msgstr "{name!r} הוא לא שדה מותאם אישית חוקי."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} לא תומך בביטוי שאילתה {expr!r}." msgstr "{data_type} לא תומך בביטוי שאילתה {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "חריגה מעומק הקינון המרבי." msgstr "חריגה מעומק הקינון המרבי."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "שדה מותאם אישית לא נמצא" msgstr "שדה מותאם אישית לא נמצא"
@@ -1339,48 +1339,48 @@ msgstr "הרצת זרימת עבודה"
msgid "workflow runs" msgid "workflow runs"
msgstr "הרצות זרימת עבודה" msgstr "הרצות זרימת עבודה"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "הרשאות אינן מספיקות." msgstr "הרשאות אינן מספיקות."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "צבע לא חוקי." msgstr "צבע לא חוקי."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "סוג קובץ %(type)s לא נתמך" msgstr "סוג קובץ %(type)s לא נתמך"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "שדה מותאם אישית id חייב להיות מספרי: %(id)s" msgstr "שדה מותאם אישית id חייב להיות מספרי: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "שדה מותאם אישית עם מזהה %(id)s איננו קיים" msgstr "שדה מותאם אישית עם מזהה %(id)s איננו קיים"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "שדות מותאמים אישית חייבים להיות רשימה של מספרים שלמים או אובייקט הממפה מזהים לערכים." msgstr "שדות מותאמים אישית חייבים להיות רשימה של מספרים שלמים או אובייקט הממפה מזהים לערכים."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "חלק מהשדות המותאמים אישית אינם קיימים או שהוגדרו פעמיים." msgstr "חלק מהשדות המותאמים אישית אינם קיימים או שהוגדרו פעמיים."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "משתנה לא חוקי זוהה." msgstr "משתנה לא חוקי זוהה."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "מזהי מסמכים כפולים אינם מורשים." msgstr "מזהי מסמכים כפולים אינם מורשים."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "מסמכים לא נמצאו: %(ids)s" msgstr "מסמכים לא נמצאו: %(ids)s"
@@ -1636,36 +1636,36 @@ msgstr "פרוטוקול ה-URI '{parts.scheme}' אינו מורשה. הפר
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "לא ניתן לפענח את ה URI {value}" msgstr "לא ניתן לפענח את ה URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "מזהה more_like_id אינו תקין" msgstr "מזהה more_like_id אינו תקין"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "הגדרות בינה מלאכותית שגויות." msgstr "הגדרות בינה מלאכותית שגויות."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "יש לציין רק אחד מהבאים: text, title_search, query או more_like_id." msgstr "יש לציין רק אחד מהבאים: text, title_search, query או more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "הרשאות לא מספיקות לשיתוף מסמך %(id)s." msgstr "הרשאות לא מספיקות לשיתוף מסמך %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "החבילה (Bundle) כבר נמצאת בתהליך עיבוד." msgstr "החבילה (Bundle) כבר נמצאת בתהליך עיבוד."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "חבילת קישור השיתוף עדיין בהכנה. נא לנסות שוב מאוחר יותר." msgstr "חבילת קישור השיתוף עדיין בהכנה. נא לנסות שוב מאוחר יותר."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "חבילת קישור השיתוף אינה זמינה." msgstr "חבילת קישור השיתוף אינה זמינה."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hindi\n" "Language-Team: Hindi\n"
"Language: hi_IN\n" "Language: hi_IN\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "दस्तावेज़" msgstr "दस्तावेज़"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "मान वैध JSON होना चाहिए." msgstr "मान वैध JSON होना चाहिए."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "अमान्य कस्टम फ़ील्ड क्वेरी एक्सप्रेशन" msgstr "अमान्य कस्टम फ़ील्ड क्वेरी एक्सप्रेशन"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "अमान्य एक्सप्रेशन सूची। खाली नहीं होनी चाहिए।" msgstr "अमान्य एक्सप्रेशन सूची। खाली नहीं होनी चाहिए।"
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "अमान्य लॉजिकल ऑपरेटर {op!r}" msgstr "अमान्य लॉजिकल ऑपरेटर {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "क्वेरी शर्तों की अधिकतम संख्या पार हो गई है।" msgstr "क्वेरी शर्तों की अधिकतम संख्या पार हो गई है।"
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} यह एक वैध कस्टम फ़ील्ड नहीं है।" msgstr "{name!r} यह एक वैध कस्टम फ़ील्ड नहीं है।"
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} क्वेरी एक्सप्रेशन {expr!r} का समर्थन नहीं करता है।" msgstr "{data_type} क्वेरी एक्सप्रेशन {expr!r} का समर्थन नहीं करता है।"
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "अधिकतम नेस्टिंग डेप्थ पार हो गई है।" msgstr "अधिकतम नेस्टिंग डेप्थ पार हो गई है।"
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "कस्टम फ़ील्ड नहीं मिला" msgstr "कस्टम फ़ील्ड नहीं मिला"
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Language: hr_HR\n" "Language: hr_HR\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumenti" msgstr "Dokumenti"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Vrijednost mora biti važeći JSON." msgstr "Vrijednost mora biti važeći JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Nevažeći izraz upita prilagođenog polja" msgstr "Nevažeći izraz upita prilagođenog polja"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Nevažeći popis izraza. Ne smije biti prazno." msgstr "Nevažeći popis izraza. Ne smije biti prazno."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Nevažeći logički operator {op!r}" msgstr "Nevažeći logički operator {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Premašen je maksimalan broj uvjeta upita." msgstr "Premašen je maksimalan broj uvjeta upita."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} nije važeće prilagođeno polje." msgstr "{name!r} nije važeće prilagođeno polje."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} ne podržava upit izraz {expr!r}." msgstr "{data_type} ne podržava upit izraz {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Premašena je najveća razina ugniježđivanja." msgstr "Premašena je najveća razina ugniježđivanja."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Prilagođeno polje nije pronađeno" msgstr "Prilagođeno polje nije pronađeno"
@@ -1338,48 +1338,48 @@ msgstr "pokretanje tijeka rada"
msgid "workflow runs" msgid "workflow runs"
msgstr "tijek rada pokrenut" msgstr "tijek rada pokrenut"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Nedovoljne ovlasti." msgstr "Nedovoljne ovlasti."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Nevažeća boja." msgstr "Nevažeća boja."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Vrsta datoteke %(type)s nije podržana" msgstr "Vrsta datoteke %(type)s nije podržana"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "ID prilagođenog polja mora biti cijeli broj: %(id)s" msgstr "ID prilagođenog polja mora biti cijeli broj: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Prilagođeno polje s ID-om %(id)s ne postoji" msgstr "Prilagođeno polje s ID-om %(id)s ne postoji"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Prilagođena polja moraju biti popis cijelih brojeva ili ID-ova objekata koji preslikavaju vrijednosti." msgstr "Prilagođena polja moraju biti popis cijelih brojeva ili ID-ova objekata koji preslikavaju vrijednosti."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Neka prilagođena polja ne postoje ili su navedena dvaput." msgstr "Neka prilagođena polja ne postoje ili su navedena dvaput."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Otkrivena je nevaljana vrsta datoteke." msgstr "Otkrivena je nevaljana vrsta datoteke."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Duplicirani identifikatori dokumenata nisu dopušteni." msgstr "Duplicirani identifikatori dokumenata nisu dopušteni."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumenti nisu pronađeni: %(ids)s" msgstr "Dokumenti nisu pronađeni: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "URI shema '{parts.scheme}' nije dopuštena. Dopuštene sheme: {', '.join
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "Nije moguće raščlaniti URI {value}" msgstr "Nije moguće raščlaniti URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Nevažeći more_like_id" msgstr "Nevažeći more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Nevažeća AI konfiguracija." msgstr "Nevažeća AI konfiguracija."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Navedite samo jedno od: text, title_search, query ili more_like_id." msgstr "Navedite samo jedno od: text, title_search, query ili more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Nedovoljne ovlasti za dijeljenje dokumenta %(id)s." msgstr "Nedovoljne ovlasti za dijeljenje dokumenta %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paket se već obrađuje." msgstr "Paket se već obrađuje."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Paket linka za dijeljenje se još priprema. Pokušajte ponovo kasnije." msgstr "Paket linka za dijeljenje se još priprema. Pokušajte ponovo kasnije."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Paket linka za dijeljenje nije dostupan." msgstr "Paket linka za dijeljenje nije dostupan."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Language: hu_HU\n" "Language: hu_HU\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumentumok" msgstr "Dokumentumok"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Érvényes JSON érték szükséges." msgstr "Érvényes JSON érték szükséges."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Érvénytelen egyéni mező lekérdezési kifejezés" msgstr "Érvénytelen egyéni mező lekérdezési kifejezés"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Érvénytelen kifejezéslista. Nem lehet üres." msgstr "Érvénytelen kifejezéslista. Nem lehet üres."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Érvénytelen logikai operátor {op!r}" msgstr "Érvénytelen logikai operátor {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maximum lekérdezési feltételszám átlépve." msgstr "Maximum lekérdezési feltételszám átlépve."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} nem érvényes egyéni mező." msgstr "{name!r} nem érvényes egyéni mező."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "A(z) {data_type} nem támogatja a {expr!r} kifejezés lekérdezést." msgstr "A(z) {data_type} nem támogatja a {expr!r} kifejezés lekérdezést."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maximum beágyazási mélység túllépve." msgstr "Maximum beágyazási mélység túllépve."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Az egyéni mező nem található" msgstr "Az egyéni mező nem található"
@@ -1338,48 +1338,48 @@ msgstr "munkafolyamat futtatás"
msgid "workflow runs" msgid "workflow runs"
msgstr "munkafolyamat futtatások" msgstr "munkafolyamat futtatások"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Nincs jogosúltsága." msgstr "Nincs jogosúltsága."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Érvénytelen szín." msgstr "Érvénytelen szín."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "%(type)s fájltípus nem támogatott" msgstr "%(type)s fájltípus nem támogatott"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Az egyéni mező azonosítójának egész számnak kell lennie: %(id)s" msgstr "Az egyéni mező azonosítójának egész számnak kell lennie: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "A(z) %(id)s azonosítójú egyéni mező nem létezik" msgstr "A(z) %(id)s azonosítójú egyéni mező nem létezik"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Az egyéni mezőknek egész számok listájának vagy azonosítókat értékekhez rendelő objektumnak kell lenniük." msgstr "Az egyéni mezőknek egész számok listájának vagy azonosítókat értékekhez rendelő objektumnak kell lenniük."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Néhány egyéni mező nem létezik, vagy kétszer lett megadva." msgstr "Néhány egyéni mező nem létezik, vagy kétszer lett megadva."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Érvénytelen változó észlelve." msgstr "Érvénytelen változó észlelve."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "A dokumentumazonosítók duplikálása nem megengedett." msgstr "A dokumentumazonosítók duplikálása nem megengedett."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumentumok nem találhatók: %(ids)s" msgstr "Dokumentumok nem találhatók: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "A '{parts.scheme}' séma nem engedélyezett. Engedélyezett sémák: {',
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "A {value} URI értelmezése sikertelen" msgstr "A {value} URI értelmezése sikertelen"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Érvénytelen more_like_id" msgstr "Érvénytelen more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Érvénytelen MI konfiguráció." msgstr "Érvénytelen MI konfiguráció."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "A text, title_search, query, vagy more_like_id közül csak egyet adjon meg." msgstr "A text, title_search, query, vagy more_like_id közül csak egyet adjon meg."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Nincs megfelelő jogosultság a %(id)s dokumentum megosztásához." msgstr "Nincs megfelelő jogosultság a %(id)s dokumentum megosztásához."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "A csomag feldolgozása már folyamatban van." msgstr "A csomag feldolgozása már folyamatban van."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "A megosztási linkcsomag készítése folyamatban. Kérjük, próbálja meg később." msgstr "A megosztási linkcsomag készítése folyamatban. Kérjük, próbálja meg később."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "A megosztási linkcsomag nem elérhető." msgstr "A megosztási linkcsomag nem elérhető."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Indonesian\n" "Language-Team: Indonesian\n"
"Language: id_ID\n" "Language: id_ID\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumen" msgstr "Dokumen"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Nilai harus berupa JSON yang valid." msgstr "Nilai harus berupa JSON yang valid."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ekspresi pencarian bidang khusus tidak valid" msgstr "Ekspresi pencarian bidang khusus tidak valid"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Daftar ekspresi tidak valid. Tidak boleh kosong." msgstr "Daftar ekspresi tidak valid. Tidak boleh kosong."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Operator logika {op!r} tidak valid" msgstr "Operator logika {op!r} tidak valid"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Jumlah maksimal kondisi pencarian terlampaui." msgstr "Jumlah maksimal kondisi pencarian terlampaui."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} bukan bidang khusus yang valid." msgstr "{name!r} bukan bidang khusus yang valid."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} tidak mendukung ekspresi pencarian expr {expr!r}." msgstr "{data_type} tidak mendukung ekspresi pencarian expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Kedalaman susunan maksimal terlampaui." msgstr "Kedalaman susunan maksimal terlampaui."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Bidang khusus tidak ditemukan" msgstr "Bidang khusus tidak ditemukan"
@@ -1338,48 +1338,48 @@ msgstr "jalankan alur kerja"
msgid "workflow runs" msgid "workflow runs"
msgstr "daftar jalankan alur kerja" msgstr "daftar jalankan alur kerja"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Izin tidak mencukupi" msgstr "Izin tidak mencukupi"
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Warna tidak sesuai." msgstr "Warna tidak sesuai."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Jenis berkas %(type)s tidak didukung" msgstr "Jenis berkas %(type)s tidak didukung"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Id kolom kustom harus berupa bilangan bulat: %(id)s" msgstr "Id kolom kustom harus berupa bilangan bulat: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Kolom kustom dengan id %(id)s tidak ada" msgstr "Kolom kustom dengan id %(id)s tidak ada"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Kolom kustom harus berupa daftar bilangan bulat atau objek yang memetakan id ke nilai." msgstr "Kolom kustom harus berupa daftar bilangan bulat atau objek yang memetakan id ke nilai."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Beberapa kolom kustom tidak ada atau ditentukan dua kali." msgstr "Beberapa kolom kustom tidak ada atau ditentukan dua kali."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Variabel ilegal terdeteksi." msgstr "Variabel ilegal terdeteksi."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Penggunaan pengenal dokumen ganda tidak diperbolehkan." msgstr "Penggunaan pengenal dokumen ganda tidak diperbolehkan."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumen tidak ditemukan: %(ids)s" msgstr "Dokumen tidak ditemukan: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "Skema URI '{parts.scheme}' tidak diizinkan. Skema yang diizinkan: {', '.
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "Gagal membaca URI {value}" msgstr "Gagal membaca URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Izin tidak mencukupi untuk berbagi dokumen %(id)s" msgstr "Izin tidak mencukupi untuk berbagi dokumen %(id)s"
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paket sedang diproses." msgstr "Paket sedang diproses."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Bundel tautan berbagi masih dalam proses persiapan. Silakan coba lagi nanti." msgstr "Bundel tautan berbagi masih dalam proses persiapan. Silakan coba lagi nanti."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Bundel tautan berbagi tidak tersedia." msgstr "Bundel tautan berbagi tidak tersedia."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Language: it_IT\n" "Language: it_IT\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Documenti" msgstr "Documenti"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Il valore deve essere un JSON valido." msgstr "Il valore deve essere un JSON valido."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Campo personalizzato della query non valido" msgstr "Campo personalizzato della query non valido"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Elenco delle espressioni non valido. Deve essere non vuoto." msgstr "Elenco delle espressioni non valido. Deve essere non vuoto."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Operatore logico non valido {op!r}" msgstr "Operatore logico non valido {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Numero massimo di condizioni di query superato." msgstr "Numero massimo di condizioni di query superato."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} non è un campo personalizzato valido." msgstr "{name!r} non è un campo personalizzato valido."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} Non supporta la jQuery Expo {Expo!r}." msgstr "{data_type} Non supporta la jQuery Expo {Expo!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Profondità massima di nidificazione superata." msgstr "Profondità massima di nidificazione superata."
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Campo personalizzato non trovato" msgstr "Campo personalizzato non trovato"
@@ -1338,48 +1338,48 @@ msgstr "esecuzione del flusso di lavoro"
msgid "workflow runs" msgid "workflow runs"
msgstr "esecuzioni del flusso di lavoro" msgstr "esecuzioni del flusso di lavoro"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Autorizzazioni insufficienti." msgstr "Autorizzazioni insufficienti."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Colore non valido." msgstr "Colore non valido."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Il tipo di file %(type)s non è supportato" msgstr "Il tipo di file %(type)s non è supportato"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "L'ID del campo personalizzato deve essere un numero intero: %(id)s" msgstr "L'ID del campo personalizzato deve essere un numero intero: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Il campo personalizzato con ID %(id)s non esiste" msgstr "Il campo personalizzato con ID %(id)s non esiste"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "I campi personalizzati devono essere un elenco di numeri interi o un oggetto che mappa gli ID ai valori." msgstr "I campi personalizzati devono essere un elenco di numeri interi o un oggetto che mappa gli ID ai valori."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Alcuni campi personalizzati non esistono o sono stati specificati due volte." msgstr "Alcuni campi personalizzati non esistono o sono stati specificati due volte."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Variabile non valida rilevata." msgstr "Variabile non valida rilevata."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Non sono consentiti identificatori di documenti duplicati." msgstr "Non sono consentiti identificatori di documenti duplicati."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Documenti non trovati: %(ids)s" msgstr "Documenti non trovati: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "Lo schema URI '{parts.scheme}' non è consentito. Schemi consentiti: {',
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "Impossibile analizzare l'URI {value}" msgstr "Impossibile analizzare l'URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "more_like_id non valido" msgstr "more_like_id non valido"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Configurazione AI non valida." msgstr "Configurazione AI non valida."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "Richiesta di backend AI scaduta." msgstr "Richiesta di backend AI scaduta."
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Specificare solo uno tra text, title_search, query o more_like_id." msgstr "Specificare solo uno tra text, title_search, query o more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Autorizzazioni insufficienti per condividere il documento %(id)s." msgstr "Autorizzazioni insufficienti per condividere il documento %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Il pacchetto è già in fase di elaborazione." msgstr "Il pacchetto è già in fase di elaborazione."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Il pacchetto di link di condivisione è ancora in fase di preparazione. Riprova più tardi." msgstr "Il pacchetto di link di condivisione è ancora in fase di preparazione. Riprova più tardi."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Il pacchetto di link di condivisione non è disponibile." msgstr "Il pacchetto di link di condivisione non è disponibile."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-08 14:28+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-08 14:29\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Language: ja_JP\n" "Language: ja_JP\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "ドキュメント" msgstr "ドキュメント"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "値は有効なJSONである必要があります。" msgstr "値は有効なJSONである必要があります。"
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "無効なカスタムフィールドクエリ式" msgstr "無効なカスタムフィールドクエリ式"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "無効な式リストです。空であってはなりません。" msgstr "無効な式リストです。空であってはなりません。"
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "無効な論理演算子 {op!r}" msgstr "無効な論理演算子 {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "クエリ条件の最大数を超えました。" msgstr "クエリ条件の最大数を超えました。"
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} は有効なカスタムフィールドではありません。" msgstr "{name!r} は有効なカスタムフィールドではありません。"
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} はクエリ expr {expr!r} をサポートしていません。" msgstr "{data_type} はクエリ expr {expr!r} をサポートしていません。"
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "最大ネストの深さを超えました。" msgstr "最大ネストの深さを超えました。"
#: documents/filters.py:1094 #: documents/filters.py:1073
msgid "Custom field not found" msgid "Custom field not found"
msgstr "カスタムフィールドが見つかりません" msgstr "カスタムフィールドが見つかりません"
@@ -1338,48 +1338,48 @@ msgstr "ワークフローの実行"
msgid "workflow runs" msgid "workflow runs"
msgstr "ワークフローの実行" msgstr "ワークフローの実行"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "無効な色" msgstr "無効な色"
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "ファイルタイプ %(type)s はサポートされていません" msgstr "ファイルタイプ %(type)s はサポートされていません"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "無効な変数を検出しました" msgstr "無効な変数を検出しました"
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""

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