Compare commits

..
Author SHA1 Message Date
Crowdin Bot 7164e7bbf8 New Crowdin translations by GitHub Action 2026-11-04 19:08:25 +00:00
GitHub Actions 057aee8bbd Auto translate strings 2026-11-04 19:07:00 +00:00
shamoon fa83a4c5dc Fix: check descendant tag permissions on delete (#14687) 2026-11-04 11:05:48 -08:00
shamoon 22746aa202 Fix: lock rows for first-user signup (#14688) 2026-11-04 16:33:28 +00:00
shamoon 137342c1f9 Documentation: clarify metadata exposure reporting scope 2026-11-04 07:29:41 -08:00
GitHub Actions 0ae5e2d3d4 Auto translate strings 2026-11-04 14:47:43 +00:00
shamoon b7b0de100f Fix: prevent ID badge wrapping under title on mobile (#14682) 2026-11-04 06:46:22 -08:00
Trenton H 8531d3e0c9 chore(deps): bump django-allauth to 65.21.1 (#14672)
django-allauth 65.20.1 -> 65.21.1. Security fixes:

- MFA: authentication records from an inactive user's passwordless WebAuthn login could carry over to a later login in the same session, skipping MFA
- Rate limits with multiple windows for the same scope shared a cache entry, allowing longer windows to be bypassed
- Passwordless WebAuthn registration and login now always require user verification (MFA_WEBAUTHN_USER_VERIFICATION)
- Headless: reaching the phone verification attempt limit now aborts pending flows

Released 2026-10-30, inside the usual 7 day dependency cooldown, taken deliberately for the security fixes.
2026-11-04 01:25:10 +00:00
Trenton H 05f7545a4a chore(deps): bump dev tooling (#14671)
- prek 0.5.3 -> 0.5.7
- ruff 0.16.9 -> 0.16.10
- faker 40.39.0 -> 40.43.0
- pytest-env 1.7.1 -> 1.8.1
- pytest-mock 3.15.1 -> 3.17.0
- time-machine 3.5.1 -> 3.6.0
- zensical 0.0.65 -> 0.0.73
- python-dotenv 1.2.3 -> 1.2.4 (pulled in by the above)
2026-11-04 00:53:22 +00:00
130 changed files with 1508 additions and 3281 deletions

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ The following are not generally considered vulnerabilities unless accompanied by
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
- missing limits or hardening settings presented without concrete impact
- generic AI or static-analysis output that is not confirmed against the current codebase and a real deployment scenario
- metadata names visible in a document's custom storage path, even when the user cannot access the underlying metadata object; this is expected behavior
- metadata names or normalized words visible in a document's custom storage path or through search/autocomplete for documents the user can access, even when the user cannot access the underlying metadata object; this is expected behavior
- the ability to attach objects that a user cannot access to a document by ID is an intentional design choice, and not considered a vulnerability
## Transparency
+4 -4
View File
@@ -27,7 +27,7 @@ dependencies = [
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.18",
"django-allauth[mfa,socialaccount]~=65.20.0",
"django-allauth[mfa,socialaccount]~=65.21.1",
"django-auditlog~=3.4.1",
"django-cachalot~=2.9.0",
"django-compression-middleware~=0.5.0",
@@ -112,14 +112,14 @@ lint = [
testing = [
"daphne",
"factory-boy~=3.3.1",
"faker>=40.36,<40.40",
"faker>=40.36,<40.44",
"imagehash",
"pytest~=9.1.1",
"pytest-cov~=7.1.0",
"pytest-django>=4.12,<4.15",
"pytest-env~=1.7.0",
"pytest-env~=1.8.1",
"pytest-httpx",
"pytest-mock~=3.15.1",
"pytest-mock~=3.17.0",
"pytest-randomly~=5.0.0",
"pytest-rerunfailures~=16.4",
"pytest-sugar",
+5 -5
View File
@@ -839,7 +839,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1141,7 +1141,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7156,14 +7156,14 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="5392341774767336507" datatype="html">
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7198,7 +7198,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -1,16 +1,18 @@
<div class="row pt-3 pb-2 align-items-center">
<div class="col-md text-truncate">
<h3 class="d-flex align-items-center mb-2 mb-md-1" style="line-height: 1.4">
<span class="text-truncate">{{title()}}</span>
@if (id()) {
<button type="button" class="badge border-0 bg-primary text-primary-text-contrast ms-3 small fs-normal" (click)="copyID()" title="Copy ID" i18n-title>
@if (copied()) {
<pngx-bs-icon width="1em" height="1em" name="clipboard-check" class="me-1"></pngx-bs-icon><ng-container i18n>Copied!</ng-container>
} @else {
ID: {{id()}}
}
</button>
}
<span class="d-flex align-items-center mw-100">
<span class="text-truncate">{{title()}}</span>
@if (id()) {
<button type="button" class="badge border-0 bg-primary text-primary-text-contrast ms-3 small fs-normal flex-shrink-0 text-nowrap" (click)="copyID()" title="Copy ID" i18n-title>
@if (copied()) {
<pngx-bs-icon width="1em" height="1em" name="clipboard-check" class="me-1"></pngx-bs-icon><ng-container i18n>Copied!</ng-container>
} @else {
ID: {{id()}}
}
</button>
}
</span>
@if (subTitle()) {
<span class="page-subtitle h6 mb-0 mt-1 fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle()}}</span>
}
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+8 -8
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="translated">ID kopieren</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -9811,7 +9811,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">454</context>
</context-group>
<target state="needs-translation">More save options</target>
<target state="translated">Weitere Speicheroptionen</target>
</trans-unit>
<trans-unit id="1309556917227148591" datatype="html" approved="yes">
<source>Document loading...</source>
@@ -11478,7 +11478,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">423</context>
</context-group>
<target state="needs-translation">View "<x id="PH" equiv-text="view.name ?? title"/>" saved successfully.</target>
<target state="translated">Ansicht "<x id="PH" equiv-text="view.name ?? title"/>" gespeichert.</target>
</trans-unit>
<trans-unit id="4646273665293421938" datatype="html">
<source>Failed to save view &quot;<x id="PH" equiv-text="title"/>&quot;.</source>
@@ -11486,7 +11486,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">429</context>
</context-group>
<target state="needs-translation">Failed to save view "<x id="PH" equiv-text="title"/>".</target>
<target state="translated">Ansicht "<x id="PH" equiv-text="title"/>" konnte nicht gespeichert werden.</target>
</trans-unit>
<trans-unit id="6837554170707123455" datatype="html" approved="yes">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+9 -9
View File
@@ -17,7 +17,7 @@
<context context-type="linenumber">386,393</context>
</context-group>
<note priority="1" from="description">Currently selected slide number read by screen reader</note>
<target state="needs-translation"> Slide <x id="INTERPOLATION" equiv-text="source?: NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="s._cycleToSelec"/> </target>
<target state="translated"> Slide <x id="INTERPOLATION" equiv-text="source?: NgbSli"/> di <x id="INTERPOLATION_1" equiv-text="s._cycleToSelec"/> </target>
</trans-unit>
<trans-unit id="ngb.carousel.previous" datatype="html">
<source>Previous</source>
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="translated">Copia ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -9811,7 +9811,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">454</context>
</context-group>
<target state="needs-translation">More save options</target>
<target state="translated">Altre opzioni di salvataggio</target>
</trans-unit>
<trans-unit id="1309556917227148591" datatype="html">
<source>Document loading...</source>
@@ -11479,7 +11479,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">423</context>
</context-group>
<target state="needs-translation">View "<x id="PH" equiv-text="view.name ?? title"/>" saved successfully.</target>
<target state="translated">Vista "<x id="PH" equiv-text="view.name ?? title"/>" salvata con successo.</target>
</trans-unit>
<trans-unit id="4646273665293421938" datatype="html">
<source>Failed to save view &quot;<x id="PH" equiv-text="title"/>&quot;.</source>
@@ -11487,7 +11487,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">429</context>
</context-group>
<target state="needs-translation">Failed to save view "<x id="PH" equiv-text="title"/>".</target>
<target state="translated">Impossibile salvare la vista "<x id="PH" equiv-text="title"/>".</target>
</trans-unit>
<trans-unit id="6837554170707123455" datatype="html" approved="yes">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="translated">Копирај ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7852,7 +7852,7 @@ Erro ao enviar documentos por e-mail</target>
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7860,7 +7860,7 @@ Erro ao enviar documentos por e-mail</target>
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7896,7 +7896,7 @@ Erro ao enviar documentos por e-mail</target>
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+11 -11
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -4991,7 +4991,7 @@
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
<context context-type="linenumber">164</context>
</context-group>
<target state="needs-translation">Search docs...</target>
<target state="translated">Pesquisar documentos...</target>
</trans-unit>
<trans-unit id="1179184907489210406" datatype="html">
<source>Operator</source>
@@ -5863,7 +5863,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
<context context-type="linenumber">53</context>
</context-group>
<target state="needs-translation">Exclude files matching</target>
<target state="translated">Excluir ficheiros que coincidem com</target>
</trans-unit>
<trans-unit id="9216117865911519658" datatype="html">
<source>Action</source>
@@ -5879,7 +5879,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
<context context-type="linenumber">59</context>
</context-group>
<target state="needs-translation">Only performed if the mail is processed.</target>
<target state="translated">Apenas executado se o e-mail for processado.</target>
</trans-unit>
<trans-unit id="1261794314435932203" datatype="html">
<source>Action parameter</source>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -11387,7 +11387,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">281</context>
</context-group>
<target state="needs-translation">Sort by number of pages</target>
<target state="translated">Ordenar por número de páginas</target>
</trans-unit>
<trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source>
@@ -12555,7 +12555,7 @@
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
<context context-type="linenumber">282</context>
</context-group>
<target state="needs-translation">Rule "<x id="PH" equiv-text="rule.name"/>" enabled.</target>
<target state="translated">Regra "<x id="PH" equiv-text="rule.name"/>" ativada.</target>
</trans-unit>
<trans-unit id="7171685227222299542" datatype="html">
<source>Rule &quot;<x id="PH" equiv-text="rule.name"/>&quot; disabled.</source>
@@ -12563,7 +12563,7 @@
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
<context context-type="linenumber">283</context>
</context-group>
<target state="needs-translation">Rule "<x id="PH" equiv-text="rule.name"/>" disabled.</target>
<target state="translated">Regra "<x id="PH" equiv-text="rule.name"/>" desativada.</target>
</trans-unit>
<trans-unit id="7238791203524413596" datatype="html">
<source>Error toggling rule &quot;<x id="PH" equiv-text="rule.name"/>&quot;.</source>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7852,7 +7852,7 @@ zhodujúce sa</target>
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7860,7 +7860,7 @@ zhodujúce sa</target>
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7896,7 +7896,7 @@ zhodujúce sa</target>
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="translated">Kopiraj ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="translated">Kopiera ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7853,7 +7853,7 @@ tüm <x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> krite
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7861,7 +7861,7 @@ tüm <x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> krite
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7897,7 +7897,7 @@ tüm <x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> krite
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7883,7 +7883,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7891,7 +7891,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7927,7 +7927,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+5 -5
View File
@@ -913,7 +913,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -1245,7 +1245,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -7851,7 +7851,7 @@
<source>Copy ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">7</context>
</context-group>
<target state="needs-translation">Copy ID</target>
</trans-unit>
@@ -7859,7 +7859,7 @@
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7895,7 +7895,7 @@
<source>Read more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+7 -18
View File
@@ -15,6 +15,7 @@ from django.db import transaction
from django.db.models import Max
from django.db.models import Q
from django.utils import timezone
from filelock import FileLock
from rest_framework.reverse import reverse
from documents.barcodes import read_barcode_values
@@ -26,10 +27,7 @@ from documents.file_handling import create_source_path_directory
from documents.file_handling import generate_filename
from documents.file_handling import generate_unique_filename
from documents.file_handling import validate_path_in_root
from documents.file_moves import defer_file_moves
from documents.file_moves import flush_pending_file_moves
from documents.loggers import LoggingMixin
from documents.media_lock import media_lock
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@@ -607,13 +605,7 @@ class ConsumerPlugin(
# now that everything is done, we can start to store the document
# in the system. This will be a transaction and reasonably fast.
try:
# File moves requested in here run after commit and after
# media.lock is released. media.lock is taken before the
# transaction: the version path locks the root row with
# select_for_update, and workflow email actions and file
# placement below re-enter media.lock instead of waiting
# on it while rows are locked.
with defer_file_moves(), media_lock(), transaction.atomic():
with transaction.atomic():
# store the document.
if self.input_doc.root_document_id:
# If this is a new version of an existing document, we need
@@ -703,7 +695,7 @@ class ConsumerPlugin(
# After everything is in the database, copy the files into
# place. If this fails, we'll also rollback the transaction.
with media_lock():
with FileLock(settings.MEDIA_LOCK):
generated_filename = generate_unique_filename(document)
if (
len(str(generated_filename))
@@ -767,9 +759,10 @@ class ConsumerPlugin(
document.archive_path,
)
# Any filename follow-up from this save is deferred by
# the scope above until after commit.
document.save(write_file_paths=True)
# Don't save with the lock active. Saving will cause the file
# renaming logic to acquire the lock as well.
# This triggers things like file renaming
document.save()
if document.root_document_id:
document_updated.send(
@@ -810,10 +803,6 @@ class ConsumerPlugin(
exception=e,
)
# The storage scope above nests under the consume task's scope, so its
# moves are still pending; place the file before the script reads its path.
flush_pending_file_moves()
self.run_post_consume_script(document)
self.log.info(f"Document {document} consumption finished")
-462
View File
@@ -1,462 +0,0 @@
"""
Document file moves, coalesced to once per unit of work.
Every save that can change a document's generated filename asks for a file
move through request_file_move(). Inside a scope opened by defer_file_moves()
(a request, a Celery task, the consumer, or a per-item command step) the
request is only recorded, and the outermost scope moves each recorded document
once, after its own writes have committed. Outside any scope the move happens
immediately.
The mover always takes media.lock first and then reads the document fresh, so
it starts from the latest committed state and never waits on media.lock while
its own transaction holds document rows.
"""
from __future__ import annotations
import logging
import shutil
import threading
from contextlib import asynccontextmanager
from contextlib import contextmanager
from contextvars import ContextVar
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Final
from asgiref.sync import sync_to_async
from django.conf import settings
from django.db import DatabaseError
from django.db import transaction
from django.utils import timezone
from filelock import Timeout
from documents.caching import clear_document_caches
from documents.file_handling import create_source_path_directory
from documents.file_handling import delete_empty_directories
from documents.file_handling import generate_filename
from documents.file_handling import generate_unique_filename
from documents.media_lock import media_lock
from documents.models import Document
from documents.utils import compute_checksum
if TYPE_CHECKING:
from collections.abc import AsyncIterator
from collections.abc import Iterator
logger = logging.getLogger("paperless.handlers")
MEDIA_LOCK_TIMEOUT_SECONDS: Final[int] = 300
class CannotMoveFilesException(Exception):
pass
def in_user_transaction() -> bool:
"""
True when the default connection is inside an atomic block that was not
created by Django's test case machinery.
Django marks the atomic blocks TestCase (and pytest-django) wrap around
each test with _from_testcase. Ignoring them keeps tests on the same path
as production.
"""
connection = transaction.get_connection()
return any(not block._from_testcase for block in connection.atomic_blocks)
class FileMoveCollector:
"""Pending document pks for one file move scope."""
def __init__(self) -> None:
self._pending: set[int] = set()
self._lock = threading.Lock()
self.closed = False
def add(self, pk: int) -> bool:
"""Record a pending move. Returns False if the collector is closed."""
with self._lock:
if self.closed:
return False
self._pending.add(pk)
return True
def take_pending(self) -> list[int]:
"""Remove and return the pending pks, sorted, leaving the collector open."""
with self._lock:
pending = sorted(self._pending)
self._pending.clear()
return pending
def close(self) -> list[int]:
"""Close the collector and return what was still pending, sorted."""
with self._lock:
self.closed = True
pending = sorted(self._pending)
self._pending.clear()
return pending
# Only the scope context managers call set() / reset(). Everything else reads
# it and mutates the collector, so adds made in a copied context (thread or
# task hops) are visible to the scope that flushes.
_active_collector: ContextVar[FileMoveCollector | None] = ContextVar(
"paperless_file_move_collector",
default=None,
)
def _path_matches_checksum(path: Path, checksum: str | None) -> bool:
if checksum is None or not path.is_file():
return False
return compute_checksum(path) == checksum
def move_files_for_document(pk: int) -> Document | None:
"""
Move the document's original and archive to the paths its current
committed state generates, then do the same for its versions.
Returns the fresh instance with its final filename columns, or None when
the document does not exist, has no filename yet, or the lock timed out.
"""
# Cheap check without the lock: most saves during consumption happen
# before the file is placed. filename is never cleared once set, so this
# stays true under the lock. values_list() keeps the check off
# Document.global_objects.filter(), which tests patch to inject database
# errors into and observe the mover's own writes.
filename = (
Document.global_objects.values_list("filename", flat=True).filter(pk=pk).first()
)
if not filename:
return None
try:
with media_lock(timeout=MEDIA_LOCK_TIMEOUT_SECONDS):
try:
document = Document.global_objects.get(pk=pk)
except Document.DoesNotExist:
logger.debug(f"Document {pk} no longer exists, skipping file move")
return None
_move_document_files(document)
except Timeout:
logger.error(
f"Timed out after {MEDIA_LOCK_TIMEOUT_SECONDS}s waiting for "
f"{settings.MEDIA_LOCK} to move files of document {pk}; the file "
f"keeps its current name and will be renamed on its next save",
)
return None
# Keep version files in sync with root
if document.root_document_id is None:
for version_pk in Document.objects.filter(root_document_id=pk).values_list(
"pk",
flat=True,
):
move_files_for_document(version_pk)
return document
def _flush(pks: list[int]) -> None:
"""Move each document; one failure never stops the rest or raises."""
for pk in pks:
try:
move_files_for_document(pk)
except Exception:
logger.exception(f"Unexpected error moving files of document {pk}")
def _flush_after_commit(pks: list[int]) -> None:
if not pks:
return
if in_user_transaction():
transaction.on_commit(lambda: _flush(pks))
else:
_flush(pks)
def _close_and_flush(collector: FileMoveCollector) -> None:
_flush_after_commit(collector.close())
@contextmanager
def defer_file_moves() -> Iterator[None]:
"""
Collect file move requests and perform each once when the outermost scope
exits, after its own writes have committed.
"""
current = _active_collector.get()
if current is not None and not current.closed:
yield
return
collector = FileMoveCollector()
token = _active_collector.set(collector)
try:
yield
finally:
_active_collector.reset(token)
_close_and_flush(collector)
def flush_pending_file_moves() -> None:
"""
Perform the current scope's pending moves now, keeping the scope open.
From an eager task inside a request this flushes the request's collector
early, which is harmless: moves still run after commit, on fresh state.
"""
collector = _active_collector.get()
if collector is None:
return
_flush_after_commit(collector.take_pending())
def _validate_move(
document: Document,
old_path: Path,
new_path: Path,
root: Path,
) -> None:
if not new_path.is_relative_to(root):
msg = (
f"Document {document!s}: Refusing to move file outside root {root}: "
f"{new_path}."
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
if not old_path.is_file():
# Can't do anything if the old file does not exist anymore.
msg = f"Document {document!s}: File {old_path} doesn't exist."
logger.fatal(msg)
raise CannotMoveFilesException(msg)
if new_path.is_file():
# Can't do anything if the new file already exists. Skip updating file.
msg = f"Document {document!s}: Cannot rename file since target path {new_path} already exists."
logger.warning(msg)
raise CannotMoveFilesException(msg)
def _move_document_files(document: Document) -> None:
try:
old_filename = document.filename
old_source_path = document.source_path
move_original = False
original_already_moved = False
old_archive_filename = document.archive_filename
old_archive_path = document.archive_path
move_archive = False
archive_already_moved = False
candidate_filename = generate_filename(document)
if len(str(candidate_filename)) > Document.MAX_STORED_FILENAME_LENGTH:
msg = (
f"Document {document!s}: Generated filename exceeds db path "
f"limit ({len(str(candidate_filename))} > "
f"{Document.MAX_STORED_FILENAME_LENGTH}): {candidate_filename!s}"
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
candidate_source_path = (settings.ORIGINALS_DIR / candidate_filename).resolve()
if candidate_filename == Path(old_filename):
new_filename = Path(old_filename)
elif (
candidate_source_path.exists() and candidate_source_path != old_source_path
):
if not old_source_path.is_file() and _path_matches_checksum(
candidate_source_path,
document.checksum,
):
new_filename = candidate_filename
original_already_moved = True
else:
# Only fall back to unique search when there is an actual conflict
new_filename = generate_unique_filename(document)
else:
new_filename = candidate_filename
# Need to convert to string to be able to save it to the db
document.filename = str(new_filename)
move_original = old_filename != document.filename and not original_already_moved
if document.has_archive_version:
archive_candidate = generate_filename(document, archive_filename=True)
if len(str(archive_candidate)) > Document.MAX_STORED_FILENAME_LENGTH:
msg = (
f"Document {document!s}: Generated archive filename exceeds "
f"db path limit ({len(str(archive_candidate))} > "
f"{Document.MAX_STORED_FILENAME_LENGTH}): {archive_candidate!s}"
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
archive_candidate_path = (
settings.ARCHIVE_DIR / archive_candidate
).resolve()
if archive_candidate == Path(old_archive_filename):
new_archive_filename = Path(old_archive_filename)
elif (
archive_candidate_path.exists()
and archive_candidate_path != old_archive_path
):
if not old_archive_path.is_file() and _path_matches_checksum(
archive_candidate_path,
document.archive_checksum,
):
new_archive_filename = archive_candidate
archive_already_moved = True
else:
new_archive_filename = generate_unique_filename(
document,
archive_filename=True,
)
else:
new_archive_filename = archive_candidate
document.archive_filename = str(new_archive_filename)
move_archive = (
old_archive_filename != document.archive_filename
and not archive_already_moved
)
else:
move_archive = False
if not move_original and not move_archive:
updates: dict[str, object] = {}
if old_filename != document.filename:
updates["filename"] = document.filename
if old_archive_filename != document.archive_filename:
updates["archive_filename"] = document.archive_filename
if updates:
# global_objects so a trashed document's path is fixed too.
updates["modified"] = timezone.now()
# Don't save() here to prevent infinite recursion.
Document.global_objects.filter(pk=document.pk).update(**updates)
return
if move_original:
_validate_move(
document,
old_source_path,
document.source_path,
settings.ORIGINALS_DIR,
)
create_source_path_directory(document.source_path)
shutil.move(old_source_path, document.source_path)
if move_archive:
_validate_move(
document,
old_archive_path,
document.archive_path,
settings.ARCHIVE_DIR,
)
create_source_path_directory(document.archive_path)
shutil.move(old_archive_path, document.archive_path)
# Don't save() here to prevent infinite recursion.
Document.global_objects.filter(pk=document.pk).update(
filename=document.filename,
archive_filename=document.archive_filename,
modified=timezone.now(),
)
# Clear any caching for this document. Slightly overkill, but not terrible
clear_document_caches(document.pk)
except (OSError, DatabaseError, CannotMoveFilesException) as e:
logger.warning(f"Exception during file handling: {e}")
# This happens when either:
# - moving the files failed due to file system errors
# - saving to the database failed due to database errors
# In both cases, we need to revert to the original state.
# Try to move files to their original location.
try:
if move_original and document.source_path.is_file():
logger.info("Restoring previous original path")
shutil.move(document.source_path, old_source_path)
if move_archive and document.archive_path.is_file():
logger.info("Restoring previous archive path")
shutil.move(document.archive_path, old_archive_path)
except Exception:
# This is fine, since:
# A: if we managed to move source from A to B, we will also
# manage to move it from B to A. If not, we have a serious
# issue that's going to get caught by the santiy checker.
# All files remain in place and will never be overwritten,
# so this is not the end of the world.
# B: if moving the original file failed, nothing has changed
# anyway.
pass
# restore old values on the document
document.filename = old_filename
document.archive_filename = old_archive_filename
# finally, remove any empty sub folders. This will do nothing if
# something has failed above.
if not old_source_path.is_file():
delete_empty_directories(
Path(old_source_path).parent,
root=settings.ORIGINALS_DIR,
)
if document.has_archive_version and not old_archive_path.is_file():
delete_empty_directories(
Path(old_archive_path).parent,
root=settings.ARCHIVE_DIR,
)
def request_file_move(document: Document) -> None:
"""
Ask for the document's files to follow its committed state.
Inside an open scope the request is recorded and performed when the scope
ends. Otherwise the move happens now and only the final filename columns
are copied onto the caller's instance, so source_path is current after
save() while other in-memory fields are left alone.
"""
collector = _active_collector.get()
if collector is not None and collector.add(document.pk):
return
moved = move_files_for_document(document.pk)
if moved is not None:
document.filename = moved.filename
document.archive_filename = moved.archive_filename
@asynccontextmanager
async def adefer_file_moves() -> AsyncIterator[None]:
"""
Async twin of defer_file_moves().
When something was collected, the flush runs through sync_to_async: its
transaction check needs the thread owning the DB connection, and the
blocking media.lock must never run on the event loop.
"""
current = _active_collector.get()
if current is not None and not current.closed:
yield
return
collector = FileMoveCollector()
token = _active_collector.set(collector)
try:
yield
finally:
_active_collector.reset(token)
pks = collector.close()
if pks:
await sync_to_async(_flush_after_commit, thread_sensitive=True)(pks)
+3 -3
View File
@@ -4,9 +4,9 @@ from dataclasses import dataclass
from email import message_from_bytes
from pathlib import Path
from django.conf import settings
from django.core.mail import EmailMessage
from documents.media_lock import media_lock
from filelock import FileLock
@dataclass(frozen=True)
@@ -48,7 +48,7 @@ def send_email(
used_filenames: set[str] = set()
# Something could be renaming the file concurrently so it can't be attached
with media_lock():
with FileLock(settings.MEDIA_LOCK):
for attachment in attachments:
filename = _get_unique_filename(
attachment.friendly_name,
@@ -18,11 +18,10 @@ from django.core import serializers
from django.core.management.base import CommandError
from django.db import transaction
from django.utils import timezone
from filelock import FileLock
from guardian.models import GroupObjectPermission
from guardian.models import UserObjectPermission
from documents.media_lock import media_lock
if TYPE_CHECKING:
from collections.abc import Generator
@@ -329,7 +328,7 @@ class Command(CryptMixin, PaperlessCommand):
)
# Prevent any ongoing changes in the documents while exporting
with media_lock(), sink:
with FileLock(settings.MEDIA_LOCK), sink:
self.dump(sink)
def dump(self, sink: ExportSink) -> None:
@@ -29,6 +29,7 @@ from django.db.models import GeneratedField
from django.db.models import Model
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save
from filelock import FileLock
from guardian.shortcuts import clear_ct_cache
from documents.export.compression import compress_type_readable
@@ -36,7 +37,6 @@ from documents.export.compression import unreadable_method_names
from documents.file_handling import create_source_path_directory
from documents.management.commands.base import PaperlessCommand
from documents.management.commands.mixins import CryptMixin
from documents.media_lock import media_lock
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@@ -644,7 +644,7 @@ class Command(CryptMixin, PaperlessCommand):
else:
archive_path = None
with media_lock():
with FileLock(settings.MEDIA_LOCK):
if Path(document.source_path).is_file():
raise FileExistsError(document.source_path)
@@ -678,7 +678,7 @@ class Command(CryptMixin, PaperlessCommand):
f"Share link bundle {bundle.pk} does not have a valid file path.",
)
with media_lock():
with FileLock(settings.MEDIA_LOCK):
bundle_target_path.parent.mkdir(parents=True, exist_ok=True)
copy_file_with_basic_stats(
bundle_source_path,
@@ -1,4 +1,5 @@
from documents.file_moves import move_files_for_document
from django.db.models.signals import post_save
from documents.management.commands.base import PaperlessCommand
from documents.models import Document
@@ -10,8 +11,5 @@ class Command(PaperlessCommand):
supports_multiprocessing = False
def handle(self, *args, **options):
for document in self.track(
Document.objects.only("pk"),
description="Renaming...",
):
move_files_for_document(document.pk)
for document in self.track(Document.objects.all(), description="Renaming..."):
post_save.send(Document, instance=document, created=False)
@@ -9,7 +9,6 @@ from rich.table import Table
from rich.text import Text
from documents.classifier import load_classifier
from documents.file_moves import defer_file_moves
from documents.management.commands.base import PaperlessCommand
from documents.models import Document
from documents.signals.handlers import set_correspondent
@@ -274,63 +273,61 @@ class Command(PaperlessCommand):
description="Retagging...",
stats_renderer=render_stats,
):
# One file move per document, after all of its assignments.
with defer_file_moves():
suggestion = DocumentSuggestion(document=document)
suggestion = DocumentSuggestion(document=document)
if do_correspondent:
correspondent = set_correspondent(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if correspondent is not None:
stats.correspondents += 1
suggestion.correspondent = correspondent
if do_correspondent:
correspondent = set_correspondent(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if correspondent is not None:
stats.correspondents += 1
suggestion.correspondent = correspondent
if do_document_type:
document_type = set_document_type(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if document_type is not None:
stats.document_types += 1
suggestion.document_type = document_type
if do_document_type:
document_type = set_document_type(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if document_type is not None:
stats.document_types += 1
suggestion.document_type = document_type
if do_tags:
tags_to_add, tags_to_remove = set_tags(
None,
document,
classifier=classifier,
replace=overwrite,
dry_run=suggest,
)
stats.tags_added += len(tags_to_add)
stats.tags_removed += len(tags_to_remove)
suggestion.tags_to_add = frozenset(tags_to_add)
suggestion.tags_to_remove = frozenset(tags_to_remove)
if do_tags:
tags_to_add, tags_to_remove = set_tags(
None,
document,
classifier=classifier,
replace=overwrite,
dry_run=suggest,
)
stats.tags_added += len(tags_to_add)
stats.tags_removed += len(tags_to_remove)
suggestion.tags_to_add = frozenset(tags_to_add)
suggestion.tags_to_remove = frozenset(tags_to_remove)
if do_storage_path:
storage_path = set_storage_path(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if storage_path is not None:
stats.storage_paths += 1
suggestion.storage_path = storage_path
if do_storage_path:
storage_path = set_storage_path(
None,
document,
classifier=classifier,
replace=overwrite,
use_first=use_first,
dry_run=suggest,
)
if storage_path is not None:
stats.storage_paths += 1
suggestion.storage_path = storage_path
stats.documents_processed += 1
stats.documents_processed += 1
if suggest:
suggestions.append(suggestion)
-36
View File
@@ -1,36 +0,0 @@
"""
The global media lock, reentrant within a thread.
Code that both needs media.lock and opens a transaction that locks document
rows must take media.lock first, so no transaction ever waits on it while
holding rows. Reentrancy lets code already holding the lock (for example the
consumer) call into code that takes it again (for example a workflow email
action attaching files) without blocking itself.
"""
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING
from django.conf import settings
from filelock import FileLock
if TYPE_CHECKING:
from collections.abc import Iterator
@contextmanager
def media_lock(timeout: float = -1) -> Iterator[None]:
"""
Hold settings.MEDIA_LOCK for the block.
is_singleton returns one FileLock instance per path in this process, and
the default thread_local=True keeps the hold count per thread, so the
same thread re-enters while other threads and processes are excluded.
The timeout goes to acquire() because a singleton rejects construction
with different parameters. A negative timeout waits forever.
"""
lock = FileLock(settings.MEDIA_LOCK, is_singleton=True)
with lock.acquire(timeout=timeout):
yield
-32
View File
@@ -1,7 +1,6 @@
import datetime
import uuid
from pathlib import Path
from typing import Any
from typing import Final
import pathvalidate
@@ -158,12 +157,6 @@ class StoragePath(MatchingModel):
class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-missing]
MAX_STORED_FILENAME_LENGTH: Final[int] = 1024
# Written only by the file mover, the consumer's file placement and the
# archiver. A full save() of a stale instance must never write them, or
# the DB can point at a path the file has already been moved away from.
FILE_PATH_FIELDS: Final[frozenset[str]] = frozenset(
{"filename", "archive_filename"},
)
# Per-component limit of common filesystems (ext4, XFS, btrfs, ZFS, APFS)
MAX_PUBLIC_FILENAME_BYTES: Final[int] = 255
@@ -375,31 +368,6 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
res += f" {self.title}"
return res
def save(self, *, write_file_paths: bool = False, **kwargs: Any) -> None:
"""
Save the document without writing filename / archive_filename on an
existing row, unless write_file_paths=True or update_fields names them.
This mirrors how Django narrows a save on a deferred instance: only the
loaded, concrete, non-generated, non-primary-key fields are written.
"""
if (
not write_file_paths
and not self._state.adding
and kwargs.get("update_fields") is None
and not kwargs.get("force_insert", False)
):
deferred = self.get_deferred_fields()
kwargs["update_fields"] = [
field.attname
for field in self._meta.concrete_fields
if not field.primary_key
and not field.generated
and field.name not in self.FILE_PATH_FIELDS
and field.attname not in deferred
]
super().save(**kwargs)
def get_effective_barcodes(self) -> list["DocumentBarcode"]:
"""
Returns the stored barcodes for the document, like
+244 -16
View File
@@ -19,22 +19,25 @@ from celery.signals import worker_process_shutdown
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.db import DatabaseError
from django.db import close_old_connections
from django.db import connections
from django.db import models
from django.db.models import Q
from django.dispatch import receiver
from django.utils import timezone
from filelock import FileLock
from rest_framework import serializers
from documents import matching
from documents.caching import clear_document_caches
from documents.caching import invalidate_llm_suggestions_cache
from documents.caching import invalidate_suggestions_cache
from documents.data_models import ConsumableDocument
from documents.file_handling import create_source_path_directory
from documents.file_handling import delete_empty_directories
from documents.file_moves import move_files_for_document
from documents.file_moves import request_file_move
from documents.media_lock import media_lock
from documents.file_handling import generate_filename
from documents.file_handling import generate_unique_filename
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@@ -52,6 +55,7 @@ from documents.models import WorkflowTrigger
from documents.permissions import restrict_queryset_to_visible
from documents.plugins.helpers import DocumentsStatusManager
from documents.templating.utils import convert_format_str_to_template_format
from documents.utils import compute_checksum
from documents.utils import copy_file_with_basic_stats
from documents.workflows.actions import build_workflow_action_context
from documents.workflows.actions import execute_email_action
@@ -335,7 +339,7 @@ def set_storage_path(
# see empty_trash in documents/tasks.py for signal handling
def cleanup_document_deletion(sender, instance, **kwargs) -> None:
with media_lock():
with FileLock(settings.MEDIA_LOCK):
if settings.EMPTY_TRASH_DIR:
# Find a non-conflicting filename in case a document with the same
# name was moved to trash earlier
@@ -401,6 +405,17 @@ def cleanup_document_deletion(sender, instance, **kwargs) -> None:
)
class CannotMoveFilesException(Exception):
pass
def _path_matches_checksum(path: Path, checksum: str | None) -> bool:
if checksum is None or not path.is_file():
return False
return compute_checksum(path) == checksum
def _filename_template_uses_custom_fields(doc: Document) -> bool:
template = None
if doc.storage_path is not None:
@@ -423,21 +438,235 @@ def update_filename_and_move_files(
instance: Document | CustomFieldInstance,
**kwargs,
) -> None:
if kwargs.get("reverse"):
# tag.documents.add(doc): instance is the Tag, not a Document.
return
action = kwargs.get("action")
if action is not None and action not in {"post_add", "post_remove", "post_clear"}:
# m2m_changed fires pre_* and post_* actions; only post_* describe the
# relation state the filename should follow.
return
if isinstance(instance, CustomFieldInstance):
if not _filename_template_uses_custom_fields(instance.document):
return
instance = instance.document
request_file_move(instance)
def validate_move(instance, old_path: Path, new_path: Path, root: Path) -> None:
if not new_path.is_relative_to(root):
msg = (
f"Document {instance!s}: Refusing to move file outside root {root}: "
f"{new_path}."
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
if not old_path.is_file():
# Can't do anything if the old file does not exist anymore.
msg = f"Document {instance!s}: File {old_path} doesn't exist."
logger.fatal(msg)
raise CannotMoveFilesException(msg)
if new_path.is_file():
# Can't do anything if the new file already exists. Skip updating file.
msg = f"Document {instance!s}: Cannot rename file since target path {new_path} already exists."
logger.warning(msg)
raise CannotMoveFilesException(msg)
if not instance.filename:
# Can't update the filename if there is no filename to begin with
# This happens when the consumer creates a new document.
# The document is modified and saved multiple times, and only after
# everything is done (i.e., the generated filename is final),
# filename will be set to the location where the consumer has put
# the file.
#
# This will in turn cause this logic to move the file where it belongs.
return
with FileLock(settings.MEDIA_LOCK):
# If this was waiting for the lock, the filename or archive_filename
# of this document may have been updated. This happens if multiple updates
# get queued from the UI for the same document
# So freshen up the data before doing anything
instance.refresh_from_db()
try:
old_filename = instance.filename
old_source_path = instance.source_path
move_original = False
original_already_moved = False
old_archive_filename = instance.archive_filename
old_archive_path = instance.archive_path
move_archive = False
archive_already_moved = False
candidate_filename = generate_filename(instance)
if len(str(candidate_filename)) > Document.MAX_STORED_FILENAME_LENGTH:
msg = (
f"Document {instance!s}: Generated filename exceeds db path "
f"limit ({len(str(candidate_filename))} > "
f"{Document.MAX_STORED_FILENAME_LENGTH}): {candidate_filename!s}"
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
candidate_source_path = (
settings.ORIGINALS_DIR / candidate_filename
).resolve()
if candidate_filename == Path(old_filename):
new_filename = Path(old_filename)
elif (
candidate_source_path.exists()
and candidate_source_path != old_source_path
):
if not old_source_path.is_file() and _path_matches_checksum(
candidate_source_path,
instance.checksum,
):
new_filename = candidate_filename
original_already_moved = True
else:
# Only fall back to unique search when there is an actual conflict
new_filename = generate_unique_filename(instance)
else:
new_filename = candidate_filename
# Need to convert to string to be able to save it to the db
instance.filename = str(new_filename)
move_original = (
old_filename != instance.filename and not original_already_moved
)
if instance.has_archive_version:
archive_candidate = generate_filename(instance, archive_filename=True)
if len(str(archive_candidate)) > Document.MAX_STORED_FILENAME_LENGTH:
msg = (
f"Document {instance!s}: Generated archive filename exceeds "
f"db path limit ({len(str(archive_candidate))} > "
f"{Document.MAX_STORED_FILENAME_LENGTH}): {archive_candidate!s}"
)
logger.warning(msg)
raise CannotMoveFilesException(msg)
archive_candidate_path = (
settings.ARCHIVE_DIR / archive_candidate
).resolve()
if archive_candidate == Path(old_archive_filename):
new_archive_filename = Path(old_archive_filename)
elif (
archive_candidate_path.exists()
and archive_candidate_path != old_archive_path
):
if not old_archive_path.is_file() and _path_matches_checksum(
archive_candidate_path,
instance.archive_checksum,
):
new_archive_filename = archive_candidate
archive_already_moved = True
else:
new_archive_filename = generate_unique_filename(
instance,
archive_filename=True,
)
else:
new_archive_filename = archive_candidate
instance.archive_filename = str(new_archive_filename)
move_archive = (
old_archive_filename != instance.archive_filename
and not archive_already_moved
)
else:
move_archive = False
if not move_original and not move_archive:
updates = {"modified": timezone.now()}
if old_filename != instance.filename:
updates["filename"] = instance.filename
if old_archive_filename != instance.archive_filename:
updates["archive_filename"] = instance.archive_filename
# Don't save() here to prevent infinite recursion.
Document.objects.filter(pk=instance.pk).update(**updates)
return
if move_original:
validate_move(
instance,
old_source_path,
instance.source_path,
settings.ORIGINALS_DIR,
)
create_source_path_directory(instance.source_path)
shutil.move(old_source_path, instance.source_path)
if move_archive:
validate_move(
instance,
old_archive_path,
instance.archive_path,
settings.ARCHIVE_DIR,
)
create_source_path_directory(instance.archive_path)
shutil.move(old_archive_path, instance.archive_path)
# Don't save() here to prevent infinite recursion.
Document.global_objects.filter(pk=instance.pk).update(
filename=instance.filename,
archive_filename=instance.archive_filename,
modified=timezone.now(),
)
# Clear any caching for this document. Slightly overkill, but not terrible
clear_document_caches(instance.pk)
except (OSError, DatabaseError, CannotMoveFilesException) as e:
logger.warning(f"Exception during file handling: {e}")
# This happens when either:
# - moving the files failed due to file system errors
# - saving to the database failed due to database errors
# In both cases, we need to revert to the original state.
# Try to move files to their original location.
try:
if move_original and instance.source_path.is_file():
logger.info("Restoring previous original path")
shutil.move(instance.source_path, old_source_path)
if move_archive and instance.archive_path.is_file():
logger.info("Restoring previous archive path")
shutil.move(instance.archive_path, old_archive_path)
except Exception:
# This is fine, since:
# A: if we managed to move source from A to B, we will also
# manage to move it from B to A. If not, we have a serious
# issue that's going to get caught by the santiy checker.
# All files remain in place and will never be overwritten,
# so this is not the end of the world.
# B: if moving the original file failed, nothing has changed
# anyway.
pass
# restore old values on the instance
instance.filename = old_filename
instance.archive_filename = old_archive_filename
# finally, remove any empty sub folders. This will do nothing if
# something has failed above.
if not old_source_path.is_file():
delete_empty_directories(
Path(old_source_path).parent,
root=settings.ORIGINALS_DIR,
)
if instance.has_archive_version and not old_archive_path.is_file():
delete_empty_directories(
Path(old_archive_path).parent,
root=settings.ARCHIVE_DIR,
)
# Keep version files in sync with root
if instance.root_document_id is None:
for version_doc in Document.objects.filter(root_document_id=instance.pk).only(
"pk",
):
update_filename_and_move_files(
Document,
version_doc,
)
@shared_task
@@ -462,8 +691,7 @@ def process_cf_select_update(custom_field: CustomField) -> None:
for cf_instance in custom_field.fields.select_related("document").iterator():
# Update the filename and move files if necessary
if _filename_template_uses_custom_fields(cf_instance.document):
move_files_for_document(cf_instance.document_id)
update_filename_and_move_files(CustomFieldInstance, cf_instance)
# should be disabled in /src/documents/management/commands/document_importer.py handle
+8 -26
View File
@@ -16,6 +16,7 @@ from django.db import models
from django.db import transaction
from django.db.models.signals import post_save
from django.utils import timezone
from filelock import FileLock
from documents import sanity_checker
from documents.barcodes import BarcodePlugin
@@ -41,7 +42,6 @@ from documents.double_sided import CollatePlugin
from documents.file_handling import create_source_path_directory
from documents.file_handling import generate_unique_filename
from documents.matching import prefilter_documents_by_workflowtrigger
from documents.media_lock import media_lock
from documents.models import Correspondent
from documents.models import CustomFieldInstance
from documents.models import Document
@@ -310,7 +310,6 @@ def sanity_check(*, raise_on_error: bool = True) -> str:
@shared_task
def bulk_update_documents(document_ids) -> None:
from documents.file_moves import flush_pending_file_moves
from documents.search import get_backend
document_ids = list(document_ids)
@@ -331,9 +330,6 @@ def bulk_update_documents(document_ids) -> None:
skip_ai_index=True, # bulk path calls update_llm_index once below
)
post_save.send(Document, instance=doc, created=False)
# Move this document's files now rather than at the end of the task,
# so a long bulk edit makes steady progress.
flush_pending_file_moves()
with get_backend().batch_update() as batch:
batch.add_or_update_ids(document_ids)
@@ -421,13 +417,7 @@ def update_document_content_maybe_archive_file(
thumbnail = parser.get_thumbnail(document.source_path, mime_type)
# media.lock first, then the transaction: never wait on media.lock
# while this transaction holds the document row. Also keeps the
# unique archive filename generation under the lock.
with media_lock(), transaction.atomic():
# Loaded before the (long) parse; read current metadata now
# that the lock is held, for the archive filename.
document.refresh_from_db()
with transaction.atomic():
oldDocument = Document.objects.get(pk=document.pk)
if parser.get_archive_path():
checksum = compute_checksum(parser.get_archive_path())
@@ -492,10 +482,11 @@ def update_document_content_maybe_archive_file(
modified=timezone.now(),
)
if parser.get_archive_path():
create_source_path_directory(document.archive_path)
shutil.move(parser.get_archive_path(), document.archive_path)
shutil.move(thumbnail, document.thumbnail_path)
with FileLock(settings.MEDIA_LOCK):
if parser.get_archive_path():
create_source_path_directory(document.archive_path)
shutil.move(parser.get_archive_path(), document.archive_path)
shutil.move(thumbnail, document.thumbnail_path)
document.refresh_from_db()
logger.info(
@@ -540,11 +531,7 @@ def empty_trash(doc_ids=None) -> None:
deleted_document_ids = list(documents.values_list("id", flat=True))
# Temporarily connect the cleanup handler
models.signals.post_delete.connect(cleanup_document_deletion, sender=Document)
# media.lock before Django's delete transaction: post_delete runs
# cleanup_document_deletion inside it while the rows are locked, and
# that handler re-enters the lock instead of waiting.
with media_lock():
documents.delete() # this is effectively a hard delete
documents.delete() # this is effectively a hard delete
logger.info(f"Deleted {len(deleted_document_ids)} documents from trash")
if settings.AUDIT_LOG_ENABLED:
@@ -574,8 +561,6 @@ def check_scheduled_workflows() -> None:
Once a document satisfies this condition, and recurring/non-recurring constraints are met, the workflow is run.
"""
from documents.file_moves import flush_pending_file_moves
scheduled_workflows = get_workflows_for_trigger(
WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
)
@@ -703,9 +688,6 @@ def check_scheduled_workflows() -> None:
sender=None,
document=document,
)
# Move this document's files now rather than at the end
# of the task, so a long run makes steady progress.
flush_pending_file_moves()
def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None:
+3 -3
View File
@@ -739,7 +739,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mime_type="application/pdf",
)
self.doc1.archive_filename = sample1_archive
self.doc1.save(write_file_paths=True)
self.doc1.save()
self.doc2 = Document.objects.create(
checksum="B",
title="B",
@@ -749,7 +749,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
created=date(2023, 1, 2),
)
self.doc2.archive_filename = sample2_archive
self.doc2.save(write_file_paths=True)
self.doc2.save()
self.doc3 = Document.objects.create(
checksum="C",
title="C",
@@ -774,7 +774,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
created=date(2023, 1, 3),
)
self.img_doc.archive_filename = img_doc_archive
self.img_doc.save(write_file_paths=True)
self.img_doc.save()
@staticmethod
def fake_decrypt(
+1 -160
View File
@@ -27,10 +27,6 @@ from documents.models import Document
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowActionEmail
from documents.models import WorkflowTrigger
from documents.parsers import ParseError
from documents.plugins.helpers import ProgressStatusOptions
from documents.tasks import sanity_check
@@ -42,7 +38,6 @@ from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.fakes.progress import FakeProgressManager
from paperless_testing.locks import record_media_lock_acquisitions
from paperless_testing.permissions import grant_object
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import MULTI_PAGE_IMAGES_PDF
@@ -226,160 +221,6 @@ class TestConsumer(
shutil.copy(src, dst)
return dst
@mock.patch("documents.consumer.load_classifier")
def test_consume_takes_media_lock_outside_transaction(self, m) -> None:
"""
GIVEN:
- A new document to consume
WHEN:
- It is consumed
THEN:
- media.lock is acquired
- media.lock is never first acquired inside a transaction
- The document's file is in place
"""
m.return_value = MagicMock()
with record_media_lock_acquisitions() as acquisitions:
with self.get_consumer(self.get_test_file()) as consumer:
consumer.run()
self.assertTrue(acquisitions)
self.assertFalse(any(a.in_user_transaction for a in acquisitions))
document = Document.objects.first()
assert document is not None
self.assertIsFile(document.source_path)
@mock.patch("documents.consumer.load_classifier")
def test_consume_places_file_before_post_consume_script(self, m) -> None:
"""
GIVEN:
- A new document to consume
WHEN:
- It is consumed
THEN:
- Pending file moves are flushed before the post-consume script runs
"""
m.return_value = MagicMock()
order = MagicMock()
with (
mock.patch(
"documents.consumer.flush_pending_file_moves",
order.flush,
),
mock.patch.object(
ConsumerPlugin,
"run_post_consume_script",
order.script,
),
):
with self.get_consumer(self.get_test_file()) as consumer:
consumer.run()
self.assertEqual(
[c[0] for c in order.mock_calls],
["flush", "script"],
)
@mock.patch("documents.consumer.load_classifier")
def test_consume_version_takes_media_lock_outside_transaction(self, m) -> None:
"""
GIVEN:
- An existing root document
WHEN:
- A new version of it is consumed (the root row is locked with
select_for_update inside the consumer's transaction)
THEN:
- media.lock is never first acquired inside a transaction
- The root's modified is bumped
"""
m.return_value = MagicMock()
with self.get_consumer(self.get_test_file()) as consumer:
consumer.run()
root_doc = Document.objects.first()
assert root_doc is not None
original_modified = timezone.now() - datetime.timedelta(days=1)
Document.objects.filter(pk=root_doc.pk).update(modified=original_modified)
version_file = self.get_test_file2()
status = FakeProgressManager(version_file.name, None)
overrides = DocumentMetadataOverrides(version_label="v2")
doc = ConsumableDocument(
DocumentSource.ApiUpload,
original_file=version_file,
root_document_id=root_doc.pk,
)
preflight = ConsumerPreflightPlugin(
doc,
overrides,
status, # type: ignore[arg-type]
self.dirs.scratch_dir,
"task-id",
)
preflight.setup()
preflight.run()
consumer = ConsumerPlugin(
doc,
overrides,
status, # type: ignore[arg-type]
self.dirs.scratch_dir,
"task-id",
)
consumer.setup()
try:
with record_media_lock_acquisitions() as acquisitions:
consumer.run()
finally:
consumer.cleanup()
self.assertTrue(acquisitions)
self.assertFalse(any(a.in_user_transaction for a in acquisitions))
root_doc.refresh_from_db()
self.assertGreater(root_doc.modified, original_modified)
@override_settings(
PAPERLESS_EMAIL_HOST="localhost",
EMAIL_ENABLED=True,
PAPERLESS_URL="http://localhost:8000",
)
@mock.patch("documents.consumer.load_classifier")
@mock.patch("django.core.mail.message.EmailMessage.send", return_value=1)
def test_consumption_workflow_email_runs_under_held_lock(self, send, m) -> None:
"""
GIVEN:
- A document added workflow with an email action
WHEN:
- A document is consumed (the consumer holds media.lock while
workflows run)
THEN:
- The email action re-enters media.lock and sends without blocking
"""
m.return_value = MagicMock()
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
)
email = WorkflowActionEmail.objects.create(
subject="added",
body="body",
to="someone@example.com",
include_document=True,
)
action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.EMAIL,
email=email,
)
workflow = Workflow.objects.create(name="email on add")
workflow.triggers.add(trigger)
workflow.actions.add(action)
with self.get_consumer(self.get_test_file()) as consumer:
consumer.run()
send.assert_called_once()
@override_settings(
FILENAME_FORMAT=None,
TIME_ZONE="America/Chicago",
@@ -909,7 +750,7 @@ class TestConsumer(
FILENAME_FORMAT="{correspondent}/{title}",
ARCHIVE_FILE_GENERATION="always",
)
@mock.patch("documents.file_moves.generate_unique_filename")
@mock.patch("documents.signals.handlers.generate_unique_filename")
def testFilenameHandlingUnstableFormat(self, m) -> None:
filenames = ["this", "that", "now this", "i cannot decide"]
+25 -23
View File
@@ -92,7 +92,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
# Ensure that filename is properly generated
self.assertEqual(document.filename, Path("none/none.pdf"))
document.save(write_file_paths=True)
document.save()
# test that creating dirs for the source_path creates the correct directory
create_source_path_directory(document.source_path)
@@ -190,7 +190,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
# Set a correspondent and save the document
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
document.save(write_file_paths=True)
document.save()
# Check proper handling of files
self.assertIsFile(
@@ -232,7 +232,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
disable_auditlog(),
):
m.side_effect = DatabaseError()
document.save(write_file_paths=True)
document.save()
# Check proper handling of files
self.assertIsFile(document.source_path)
@@ -336,7 +336,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
# Ensure that filename is properly generated
document.filename = generate_filename(document)
document.save(write_file_paths=True)
document.save()
self.assertEqual(document.filename, "none/none.pdf")
create_source_path_directory(document.source_path)
@@ -362,7 +362,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
# Ensure that filename is properly generated
document.filename = generate_filename(document)
document.save(write_file_paths=True)
document.save()
self.assertEqual(document.filename, "none/none.pdf")
create_source_path_directory(document.source_path)
@@ -385,7 +385,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
document.save()
document.filename = generate_filename(document)
document.save(write_file_paths=True)
document.save()
create_source_path_directory(document.source_path)
Path(document.source_path).touch()
document.delete()
@@ -421,7 +421,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
# Set a correspondent and save the document
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
document.save(write_file_paths=True)
document.save()
# Check proper handling of files
self.assertIsDir(settings.ORIGINALS_DIR / "test")
@@ -476,7 +476,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_filename_relative(self) -> None:
doc = Document.objects.create(title="doc1", mime_type="application/pdf")
doc.filename = generate_filename(doc)
doc.save(write_file_paths=True)
doc.save()
self.assertEqual(
doc.source_path,
@@ -532,7 +532,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
# Ensure that filename is properly generated
document.filename = generate_filename(document)
document.save(write_file_paths=True)
document.save()
self.assertEqual(document.filename, "none/none/none.pdf")
create_source_path_directory(document.source_path)
Path(document.source_path).touch()
@@ -620,13 +620,13 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
Path(document.source_path).touch()
Path(document2.source_path).touch()
document.filename = "0000001.pdf"
document.save(write_file_paths=True)
document.save()
self.assertIsFile(document.source_path)
self.assertEqual(document.filename, "qwe.pdf")
document2.filename = "0000002.pdf"
document2.save(write_file_paths=True)
document2.save()
self.assertIsFile(document.source_path)
self.assertEqual(document2.filename, "qwe_01.pdf")
@@ -656,17 +656,19 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(document2.filename, "qwe.pdf")
@override_settings(FILENAME_FORMAT="{title}")
@mock.patch("documents.file_moves.Document.global_objects.filter")
@mock.patch("documents.file_moves.shutil.move")
@mock.patch("documents.signals.handlers.Document.objects.filter")
@mock.patch("documents.signals.handlers.shutil.move")
def test_no_move_only_save(self, mock_move, mock_filter) -> None:
"""
GIVEN:
- A document with a filename
- The document is saved
- The filename is not changed
WHEN:
- The document is saved without a change that affects its path
- The document is saved
THEN:
- The document modified date is updated by the save itself
- The file is not moved and the mover writes nothing
- The document modified date is updated
- The document is not moved
"""
with disable_auditlog():
doc = Document.objects.create(
@@ -684,7 +686,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
doc.save()
doc.refresh_from_db()
mock_filter.assert_not_called()
mock_filter.assert_called()
self.assertNotEqual(original_modified, doc.modified)
mock_move.assert_not_called()
@@ -692,7 +694,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
FILENAME_FORMAT="{{title}}_{{custom_fields|get_cf_value('test')}}",
CELERY_TASK_ALWAYS_EAGER=True,
)
@mock.patch("documents.signals.handlers.move_files_for_document")
@mock.patch("documents.signals.handlers.update_filename_and_move_files")
def test_select_cf_updated(self, m) -> None:
"""
GIVEN:
@@ -2043,7 +2045,7 @@ class TestCustomFieldFilenameUpdates(
Path(self.doc.source_path).parent.mkdir(parents=True, exist_ok=True)
Path(self.doc.source_path).touch()
with mock.patch("documents.file_moves.generate_unique_filename") as m:
with mock.patch("documents.signals.handlers.generate_unique_filename") as m:
m.side_effect = generate_unique_filename
self.cfi.value_text = "updated"
self.cfi.save()
@@ -2065,7 +2067,7 @@ class TestCustomFieldFilenameUpdates(
Path(self.doc.source_path).parent.mkdir(parents=True, exist_ok=True)
Path(self.doc.source_path).touch()
with mock.patch("documents.file_moves.generate_unique_filename") as m:
with mock.patch("documents.signals.handlers.generate_unique_filename") as m:
m.side_effect = generate_unique_filename
self.cfi.value_text = "updated"
self.cfi.save()
@@ -2143,7 +2145,7 @@ def test_refresh_failure_surfaces_database_error(
GIVEN:
- A document with a stored filename
WHEN:
- Saving it triggers a filename update and loading it from the database fails
- Saving it triggers a filename update and refreshing it from the database fails
THEN:
- The database error is raised, not an unbound variable error from the rollback
"""
@@ -2151,8 +2153,8 @@ def test_refresh_failure_surfaces_database_error(
with (
mock.patch.object(
Document.global_objects,
"get",
Document,
"refresh_from_db",
side_effect=DatabaseError("database is locked"),
),
pytest.raises(DatabaseError, match="database is locked"),
-944
View File
@@ -1,944 +0,0 @@
from __future__ import annotations
import datetime
import hashlib
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from asgiref.sync import async_to_sync
from asgiref.sync import sync_to_async
from celery.contrib.django.task import DjangoTask
from django.db import transaction
from django.http import HttpRequest
from django.http import HttpResponse
from django.test import RequestFactory
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from filelock import Timeout
from rest_framework import status
from documents.file_moves import _active_collector
from documents.file_moves import defer_file_moves
from documents.file_moves import flush_pending_file_moves
from documents.file_moves import move_files_for_document
from documents.file_moves import request_file_move
from documents.models import CustomField
from documents.models import Document
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.tasks import bulk_update_documents
from documents.tasks import check_scheduled_workflows
from documents.tasks import empty_trash
from paperless.celery import app as celery_app
from paperless.middleware import FileMoveMiddleware
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import CustomFieldFactory
from paperless_testing.factories import CustomFieldInstanceFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
from paperless_testing.locks import record_media_lock_acquisitions
if TYPE_CHECKING:
from collections.abc import Callable
from contextlib import AbstractContextManager
from pytest_django.fixtures import Settings
from pytest_mock import MockerFixture
from rest_framework.test import APIClient
from paperless_testing.dirs import PaperlessDirs
OnCommitCapture = Callable[..., AbstractContextManager[list[Callable[[], object]]]]
CONTENT = b"original content"
@pytest.fixture
def title_format(settings: Settings) -> None:
settings.FILENAME_FORMAT = "{title}"
@pytest.fixture
def document(paperless_dirs: PaperlessDirs, title_format: None) -> Document:
"""A document named after its title, with its original on disk."""
(paperless_dirs.originals_dir / "original.pdf").write_bytes(CONTENT)
return DocumentFactory(
title="original",
filename="original.pdf",
mime_type="application/pdf",
checksum=hashlib.sha256(CONTENT).hexdigest(),
)
@celery_app.task(name="documents.tests.test_file_moves.rename_then_fail")
def rename_then_fail(pk: int, originals_dir: str) -> None:
"""Test-only task: rename the document, check nothing moved yet, then fail."""
doc = Document.objects.get(pk=pk)
doc.title = "renamed"
doc.save()
assert (Path(originals_dir) / "original.pdf").is_file()
raise RuntimeError("task failed after saving")
@pytest.mark.django_db
class TestMoveFilesForDocument:
def test_moves_using_fresh_db_state(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A document whose title changed in the DB without any signal
WHEN:
- move_files_for_document is called with its pk
THEN:
- The file and the DB filename follow the DB title
- The returned instance carries the final filename
"""
Document.objects.filter(pk=document.pk).update(title="renamed")
moved = move_files_for_document(document.pk)
assert moved is not None
assert moved.filename == "renamed.pdf"
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
assert not (paperless_dirs.originals_dir / "original.pdf").exists()
document.refresh_from_db()
assert document.filename == "renamed.pdf"
def test_missing_document_returns_none(self, paperless_dirs: PaperlessDirs) -> None:
"""
GIVEN:
- A pk with no document row
WHEN:
- move_files_for_document is called
THEN:
- It returns None without raising
"""
assert move_files_for_document(999_999) is None
def test_document_without_filename_skips_lock(
self,
paperless_dirs: PaperlessDirs,
title_format: None,
) -> None:
"""
GIVEN:
- A document still being consumed (no filename yet)
WHEN:
- move_files_for_document is called
THEN:
- It returns None, writes nothing, and never takes media.lock
"""
doc = DocumentFactory(filename=None, mime_type="application/pdf")
with record_media_lock_acquisitions() as acquisitions:
assert move_files_for_document(doc.pk) is None
assert acquisitions == []
doc.refresh_from_db()
assert doc.filename is None
def test_no_change_does_not_write_modified(
self,
document: Document,
) -> None:
"""
GIVEN:
- A document already at its generated path, modified in the past
WHEN:
- move_files_for_document is called
THEN:
- modified is not written
"""
past = timezone.now() - datetime.timedelta(days=1)
Document.objects.filter(pk=document.pk).update(modified=past)
move_files_for_document(document.pk)
document.refresh_from_db()
assert document.modified == past
def test_trashed_already_moved_document_gets_db_path_fixed(
self,
paperless_dirs: PaperlessDirs,
title_format: None,
) -> None:
"""
GIVEN:
- A trashed document whose DB filename is stale
- Its file already sits at the generated path (checksum matches)
WHEN:
- move_files_for_document is called
THEN:
- The DB filename is updated to where the file is
"""
doc = DocumentFactory(
title="current",
filename="stale.pdf",
mime_type="application/pdf",
checksum=hashlib.sha256(CONTENT).hexdigest(),
)
assert Document.objects.get(pk=doc.pk).filename == "stale.pdf"
doc.delete()
(paperless_dirs.originals_dir / "current.pdf").write_bytes(CONTENT)
move_files_for_document(doc.pk)
assert Document.global_objects.get(pk=doc.pk).filename == "current.pdf"
def test_lock_timeout_leaves_consistent_state(
self,
document: Document,
paperless_dirs: PaperlessDirs,
mocker: MockerFixture,
caplog: pytest.LogCaptureFixture,
) -> None:
"""
GIVEN:
- A document whose title changed in the DB
- media.lock cannot be acquired
WHEN:
- move_files_for_document is called
THEN:
- An error is logged and None is returned
- The lock was requested with the 300 second timeout
- The DB filename and the file on disk are unchanged
"""
Document.objects.filter(pk=document.pk).update(title="renamed")
lock = mocker.patch("documents.file_moves.media_lock")
lock.return_value.__enter__.side_effect = Timeout("media.lock")
with caplog.at_level(logging.ERROR, logger="paperless.handlers"):
assert move_files_for_document(document.pk) is None
assert "Timed out" in caplog.text
lock.assert_called_once_with(timeout=300)
document.refresh_from_db()
assert document.filename == "original.pdf"
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
@pytest.mark.django_db
class TestRequestFileMoveWithoutScope:
def test_moves_immediately_and_syncs_only_file_columns(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- No file move scope is open
- A caller instance with an unsaved in-memory change
WHEN:
- request_file_move is called after the title changed in the DB
THEN:
- The file moves immediately
- The caller's filename is updated
- The caller's unsaved change is kept
"""
Document.objects.filter(pk=document.pk).update(title="renamed")
unsaved = CorrespondentFactory()
document.correspondent = unsaved
request_file_move(document)
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
assert document.filename == "renamed.pdf"
assert document.correspondent == unsaved
assert document.title == "original"
@pytest.mark.django_db
class TestReceiver:
def test_reverse_m2m_does_not_request_a_move(
self,
document: Document,
mocker: MockerFixture,
) -> None:
"""
GIVEN:
- A tag
WHEN:
- A document is added through the tag's reverse relation
THEN:
- No move is requested with the tag as the instance
"""
tag = TagFactory()
request = mocker.patch("documents.signals.handlers.request_file_move")
tag.documents.add(document)
request.assert_not_called()
@pytest.mark.django_db
class TestDeferFileMoves:
def test_moves_once_on_exit(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- An open file move scope
WHEN:
- The document is saved with a new title inside the scope
THEN:
- Nothing moves while the scope is open
- On exit the file and DB filename follow the final title
"""
with defer_file_moves():
document.title = "renamed"
document.save()
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
assert Document.objects.get(pk=document.pk).filename == "original.pdf"
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
assert not (paperless_dirs.originals_dir / "original.pdf").exists()
assert Document.objects.get(pk=document.pk).filename == "renamed.pdf"
def test_nested_scope_defers_to_outer(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A scope nested inside another
WHEN:
- The document is saved inside the inner scope and the inner scope exits
THEN:
- Nothing moves until the outer scope exits
"""
with defer_file_moves():
with defer_file_moves():
document.title = "renamed"
document.save()
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_closed_collector_moves_immediately(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A collector that has already been closed (work outliving its scope)
WHEN:
- A save asks for a move while that collector is still current
THEN:
- The move happens immediately instead of being lost
"""
with defer_file_moves():
collector = _active_collector.get()
assert collector is not None
token = _active_collector.set(collector)
try:
document.title = "renamed"
document.save()
finally:
_active_collector.reset(token)
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_scope_closing_inside_atomic_flushes_on_commit(
self,
document: Document,
paperless_dirs: PaperlessDirs,
django_capture_on_commit_callbacks: OnCommitCapture,
) -> None:
"""
GIVEN:
- A scope opened and closed inside an atomic block
WHEN:
- The atomic block commits
THEN:
- The move runs only once the commit callbacks run
"""
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic(), defer_file_moves():
document.title = "renamed"
document.save()
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_scope_closing_inside_rolled_back_atomic_moves_nothing(
self,
document: Document,
paperless_dirs: PaperlessDirs,
django_capture_on_commit_callbacks: OnCommitCapture,
) -> None:
"""
GIVEN:
- A scope opened and closed inside an atomic block
WHEN:
- The atomic block rolls back
THEN:
- The title change is gone and the file is not moved
"""
class Rollback(Exception):
pass
with django_capture_on_commit_callbacks(execute=True):
with pytest.raises(Rollback):
with transaction.atomic(), defer_file_moves():
document.title = "renamed"
document.save()
raise Rollback
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
assert not (paperless_dirs.originals_dir / "renamed.pdf").exists()
assert Document.objects.get(pk=document.pk).title == "original"
def test_flush_pending_moves_early(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- An open scope with a pending move
WHEN:
- flush_pending_file_moves is called inside the scope
THEN:
- The move happens before the scope exits
"""
with defer_file_moves():
document.title = "renamed"
document.save()
flush_pending_file_moves()
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_document_without_filename_in_scope_is_noop(
self,
paperless_dirs: PaperlessDirs,
title_format: None,
) -> None:
"""
GIVEN:
- A document still being consumed (no filename yet)
WHEN:
- It is saved inside a scope and the scope exits
THEN:
- Nothing is written, media.lock is never taken, nothing raises
"""
doc = DocumentFactory(filename=None, mime_type="application/pdf")
with record_media_lock_acquisitions() as acquisitions:
with defer_file_moves():
doc.title = "renamed"
doc.save()
assert acquisitions == []
doc.refresh_from_db()
assert doc.filename is None
def test_mover_keeps_metadata_changed_after_save(
self,
document: Document,
paperless_dirs: PaperlessDirs,
settings: Settings,
) -> None:
"""
GIVEN:
- A filename format using correspondent and title
- A scope in which the title is saved
WHEN:
- Another writer changes the correspondent before the scope exits
THEN:
- The correspondent change survives the flush
- The final path reflects both changes
"""
settings.FILENAME_FORMAT = "{correspondent}/{title}"
correspondent = CorrespondentFactory(name="acme")
with defer_file_moves():
document.title = "renamed"
document.save()
Document.objects.filter(pk=document.pk).update(correspondent=correspondent)
document.refresh_from_db()
assert document.correspondent == correspondent
assert document.filename == "acme/renamed.pdf"
assert (paperless_dirs.originals_dir / "acme" / "renamed.pdf").is_file()
def test_one_failure_does_not_stop_the_flush(
self,
paperless_dirs: PaperlessDirs,
title_format: None,
mocker: MockerFixture,
) -> None:
"""
GIVEN:
- Two documents saved in one scope
- Moving the first raises an unexpected error
WHEN:
- The scope exits
THEN:
- The scope does not raise
- The second document is still moved
"""
(paperless_dirs.originals_dir / "a.pdf").write_bytes(CONTENT)
first = DocumentFactory(
filename="a.pdf",
title="a",
mime_type="application/pdf",
)
(paperless_dirs.originals_dir / "b.pdf").write_bytes(CONTENT)
second = DocumentFactory(
filename="b.pdf",
title="b",
mime_type="application/pdf",
)
real_move = move_files_for_document
def failing_for_first(pk: int) -> Document | None:
if pk == first.pk:
raise RuntimeError("boom")
return real_move(pk)
mocker.patch(
"documents.file_moves.move_files_for_document",
side_effect=failing_for_first,
)
with defer_file_moves():
first.title = "a2"
first.save()
second.title = "b2"
second.save()
assert (paperless_dirs.originals_dir / "b2.pdf").is_file()
assert (paperless_dirs.originals_dir / "a.pdf").is_file()
@pytest.mark.django_db
class TestFileMoveMiddleware:
def test_sync_mode_flushes_before_returning(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- The middleware wrapping a sync view that renames the document
WHEN:
- A request passes through it
THEN:
- Nothing moves while the view runs
- The file is at its final path when the middleware returns
"""
def view(request: HttpRequest) -> HttpResponse:
document.title = "renamed"
document.save()
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
return HttpResponse()
middleware = FileMoveMiddleware(view)
assert middleware.async_mode is False
middleware(RequestFactory().get("/"))
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_async_mode_flushes_after_await(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- The middleware wrapping an async view that saves through
sync_to_async
WHEN:
- A request passes through it
THEN:
- Nothing moves while the view runs
- The file is at its final path after the await, so the collector
survived the context copy into the sync thread
"""
def rename() -> None:
document.title = "renamed"
document.save()
async def view(request: HttpRequest) -> HttpResponse:
await sync_to_async(rename)()
assert (paperless_dirs.originals_dir / "original.pdf").is_file()
return HttpResponse()
middleware = FileMoveMiddleware(view)
assert middleware.async_mode is True
async_to_sync(middleware)(RequestFactory().get("/"))
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_view_raising_after_save_still_moves(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A view that saves a rename and then raises
WHEN:
- A request passes through the middleware
THEN:
- The exception propagates
- The committed rename is still moved
"""
def view(request: HttpRequest) -> HttpResponse:
document.title = "renamed"
document.save()
raise RuntimeError("view failed")
with pytest.raises(RuntimeError, match="view failed"):
FileMoveMiddleware(view)(RequestFactory().get("/"))
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
@pytest.mark.django_db
class TestFileMovesThroughApi:
def test_media_lock_never_taken_inside_a_transaction(
self,
admin_client: APIClient,
document: Document,
) -> None:
"""
GIVEN:
- A document and a tag
WHEN:
- A tag + title PATCH and a note POST are made through the API
THEN:
- media.lock is acquired at least once
- media.lock is never first acquired while a transaction is open
"""
tag = TagFactory()
with record_media_lock_acquisitions() as acquisitions:
response = admin_client.patch(
f"/api/documents/{document.pk}/",
{"tags": [tag.pk], "title": "renamed"},
format="json",
)
assert response.status_code == status.HTTP_200_OK
response = admin_client.post(
f"/api/documents/{document.pk}/notes/",
{"note": "a note"},
format="json",
)
assert response.status_code == status.HTTP_200_OK
assert acquisitions
assert not any(a.in_user_transaction for a in acquisitions)
def test_patch_moves_to_final_path(
self,
admin_client: APIClient,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A document named after its title
WHEN:
- Its title and tags are changed through the API
THEN:
- The file is at the final path when the response returns
"""
response = admin_client.patch(
f"/api/documents/{document.pk}/",
{"tags": [TagFactory().pk], "title": "renamed"},
format="json",
)
assert response.status_code == status.HTTP_200_OK
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
assert Document.objects.get(pk=document.pk).filename == "renamed.pdf"
def test_patch_response_reports_post_move_modified(
self,
admin_client: APIClient,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A document named after its title
WHEN:
- Its title is changed through the API, causing a rename
THEN:
- The modified time in the response equals the stored value,
which the file move bumped after the edit was saved
"""
response = admin_client.patch(
f"/api/documents/{document.pk}/",
{"title": "renamed"},
format="json",
)
assert response.status_code == status.HTTP_200_OK
stored = Document.objects.get(pk=document.pk)
assert stored.filename == "renamed.pdf"
assert parse_datetime(response.json()["modified"]) == stored.modified
def test_trash_restore_keeps_db_and_disk_consistent(
self,
admin_client: APIClient,
document: Document,
) -> None:
"""
GIVEN:
- A document moved to the trash
WHEN:
- It is restored through the trash API
THEN:
- Its DB filename points at a file that exists
"""
admin_client.delete(f"/api/documents/{document.pk}/")
response = admin_client.post(
"/api/trash/",
{"action": "restore", "documents": [document.pk]},
format="json",
)
assert response.status_code == status.HTTP_200_OK
restored = Document.objects.get(pk=document.pk)
assert restored.source_path.is_file()
def test_root_storage_path_change_moves_versions(
self,
admin_client: APIClient,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A root document with one version, both with files on disk
WHEN:
- The root's storage path is changed through the API
THEN:
- The root and version files both follow the storage path
"""
(paperless_dirs.originals_dir / "version.pdf").write_bytes(CONTENT)
version = DocumentFactory(
root_document=document,
title="version",
filename="version.pdf",
mime_type="application/pdf",
checksum=hashlib.sha256(CONTENT).hexdigest(),
)
storage_path = StoragePathFactory(path="stored/{{ title }}")
response = admin_client.patch(
f"/api/documents/{document.pk}/",
{"storage_path": storage_path.pk},
format="json",
)
assert response.status_code == status.HTTP_200_OK
document.refresh_from_db()
version.refresh_from_db()
assert document.filename.startswith("stored/")
assert version.filename.startswith("stored/")
assert document.source_path.is_file()
assert version.source_path.is_file()
def test_custom_field_change_moves_to_final_path(
self,
admin_client: APIClient,
document: Document,
paperless_dirs: PaperlessDirs,
settings: Settings,
) -> None:
"""
GIVEN:
- A filename format that uses a text custom field
- A document with that field set
WHEN:
- The field value is changed through the API
THEN:
- The file is at the path for the new value
"""
settings.FILENAME_FORMAT = (
"{{ custom_fields|get_cf_value('flavor') }}/{{ title }}"
)
field = CustomFieldFactory(
name="flavor",
data_type=CustomField.FieldDataType.STRING,
)
CustomFieldInstanceFactory(field=field, document=document, value_text="apple")
response = admin_client.patch(
f"/api/documents/{document.pk}/",
{"custom_fields": [{"field": field.pk, "value": "cherry"}]},
format="json",
)
assert response.status_code == status.HTTP_200_OK
document.refresh_from_db()
assert document.filename == "cherry/original.pdf"
assert (paperless_dirs.originals_dir / "cherry" / "original.pdf").is_file()
@pytest.mark.django_db
class TestCeleryTaskScope:
def test_task_class_opens_scope(self) -> None:
"""
GIVEN:
- The paperless Celery app
WHEN:
- A registered task is inspected
THEN:
- It runs through FileMoveScopedTask
- It keeps Celery's DjangoTask API, such as delay_on_commit
"""
from paperless.celery import FileMoveScopedTask
assert isinstance(bulk_update_documents, FileMoveScopedTask)
assert isinstance(bulk_update_documents, DjangoTask)
def test_failing_task_still_moves_committed_change(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A task that renames a document, then raises
WHEN:
- The task is applied
THEN:
- The task fails with its own error
- Nothing moved while the task body ran
- The committed rename is moved when the task ends
"""
result = rename_then_fail.apply(
args=[document.pk, str(paperless_dirs.originals_dir)],
)
assert result.failed()
assert isinstance(result.result, RuntimeError)
assert (paperless_dirs.originals_dir / "renamed.pdf").is_file()
def test_bulk_update_moves_each_document(
self,
document: Document,
paperless_dirs: PaperlessDirs,
mocker: MockerFixture,
) -> None:
"""
GIVEN:
- Two documents whose titles changed in the DB without signals
WHEN:
- bulk_update_documents runs for both
THEN:
- Each document's file follows its new title
"""
mocker.patch("documents.search.get_backend")
(paperless_dirs.originals_dir / "second.pdf").write_bytes(CONTENT)
second = DocumentFactory(
title="second",
filename="second.pdf",
mime_type="application/pdf",
checksum=hashlib.sha256(CONTENT).hexdigest(),
)
Document.objects.filter(pk=document.pk).update(title="first-renamed")
Document.objects.filter(pk=second.pk).update(title="second-renamed")
bulk_update_documents.apply(args=[[document.pk, second.pk]])
assert (paperless_dirs.originals_dir / "first-renamed.pdf").is_file()
assert (paperless_dirs.originals_dir / "second-renamed.pdf").is_file()
def test_scheduled_workflows_move_each_document(
self,
document: Document,
paperless_dirs: PaperlessDirs,
mocker: MockerFixture,
) -> None:
"""
GIVEN:
- Two documents matched by a scheduled workflow, where running the
workflow renames the document
WHEN:
- check_scheduled_workflows runs
THEN:
- The first document's file is already moved when the workflow
runs for the second, not only when the task ends
"""
(paperless_dirs.originals_dir / "second.pdf").write_bytes(CONTENT)
second = DocumentFactory(
title="second",
filename="second.pdf",
mime_type="application/pdf",
checksum=hashlib.sha256(CONTENT).hexdigest(),
)
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
schedule_offset_days=0,
schedule_date_field=WorkflowTrigger.ScheduleDateField.ADDED,
)
workflow = Workflow.objects.create(name="scheduled", order=0)
workflow.triggers.add(trigger)
workflow.actions.add(WorkflowAction.objects.create(assign_title="unused"))
seen: dict[int, bool] = {}
def rename(*, document: Document, **kwargs: object) -> None:
seen[document.pk] = (paperless_dirs.originals_dir / "first.pdf").is_file()
document.title = "first" if document.pk == first_pk else "other"
document.save()
first_pk = min(document.pk, second.pk)
mocker.patch("documents.tasks.run_workflows", side_effect=rename)
mocker.patch("documents.tasks.send_websocket_document_updated")
check_scheduled_workflows.apply()
second_pk = max(document.pk, second.pk)
assert seen[first_pk] is False
assert seen[second_pk] is True
@pytest.mark.django_db
class TestEmptyTrashLockOrdering:
def test_hard_delete_takes_media_lock_outside_transaction(
self,
document: Document,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A trashed document with its file on disk
WHEN:
- The trash is emptied for it
THEN:
- media.lock is never first acquired inside a transaction
- The document row and its file are gone
"""
document.delete()
with record_media_lock_acquisitions() as acquisitions:
empty_trash([document.pk])
assert acquisitions
assert not any(a.in_user_transaction for a in acquisitions)
assert not Document.global_objects.filter(pk=document.pk).exists()
assert not (paperless_dirs.originals_dir / "original.pdf").exists()
+1 -61
View File
@@ -18,13 +18,11 @@ if TYPE_CHECKING:
from pytest_mock import MockerFixture
from documents.file_handling import generate_filename
from documents.file_moves import move_files_for_document
from documents.models import Document
from documents.tasks import update_document_content_maybe_archive_file
from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.locks import record_media_lock_acquisitions
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
sample_file: Path = SIMPLE_DIGITAL_PDF
@@ -65,28 +63,6 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertTrue(filecmp.cmp(sample_file, doc.source_path))
self.assertEqual(doc.archive_filename, "none/A.pdf")
def test_handle_document_takes_media_lock_outside_transaction(self) -> None:
"""
GIVEN:
- A document whose archive version is being generated
WHEN:
- update_document_content_maybe_archive_file runs
THEN:
- media.lock is acquired
- media.lock is never first acquired while a transaction holds the
document row
"""
doc = self.make_models()
shutil.copy(sample_file, Path(self.dirs.originals_dir) / f"{doc.id:07}.pdf")
with record_media_lock_acquisitions() as acquisitions:
update_document_content_maybe_archive_file(doc.pk)
self.assertTrue(acquisitions)
self.assertFalse(any(a.in_user_transaction for a in acquisitions))
doc.refresh_from_db()
self.assertIsFile(doc.archive_path)
def test_unknown_mime_type(self) -> None:
doc = self.make_models()
doc.mime_type = "sdgfh"
@@ -260,7 +236,7 @@ class TestRenamer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
doc = Document.objects.create(title="test", mime_type="image/jpeg")
doc.filename = generate_filename(doc)
doc.archive_filename = generate_filename(doc, archive_filename=True)
doc.save(write_file_paths=True)
doc.save()
Path(doc.source_path).touch()
Path(doc.archive_path).touch()
@@ -277,42 +253,6 @@ class TestRenamer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertIsFile(doc2.source_path)
self.assertIsFile(doc2.archive_path)
@override_settings(FILENAME_FORMAT="")
def test_rename_moves_each_document_by_direct_call(self) -> None:
"""
GIVEN:
- Two documents whose filename format changed
WHEN:
- document_renamer runs
THEN:
- move_files_for_document is called once per document
- Both files are at their new paths
"""
docs = []
for title in ("first", "second"):
doc = Document.objects.create(title=title, mime_type="image/jpeg")
doc.filename = generate_filename(doc)
doc.save(write_file_paths=True)
Path(doc.source_path).touch()
docs.append(doc)
with (
override_settings(FILENAME_FORMAT="{title}"),
mock.patch(
"documents.management.commands.document_renamer.move_files_for_document",
wraps=move_files_for_document,
) as mover,
):
call_command("document_renamer", skip_checks=True)
self.assertCountEqual(
[c.args[0] for c in mover.call_args_list],
[d.pk for d in docs],
)
for doc in docs:
doc.refresh_from_db()
self.assertIsFile(doc.source_path)
@pytest.mark.management
class TestCreateClassifier:
@@ -4,13 +4,10 @@ Tests for the document_retagger management command.
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from django.core.management import call_command
from django.core.management.base import CommandError
from documents.file_moves import _active_collector
from documents.models import Correspondent
from documents.models import Document
from documents.models import DocumentType
@@ -24,12 +21,6 @@ from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
if TYPE_CHECKING:
from pytest_django.fixtures import Settings
from pytest_mock import MockerFixture
from paperless_testing.dirs import PaperlessDirs
# ---------------------------------------------------------------------------
# Module-level type aliases
# ---------------------------------------------------------------------------
@@ -493,69 +484,3 @@ class TestRetaggerEdgeCases(DirectoriesMixin):
assert d_first.tags.count() == 0
assert d_unrelated.tags.count() == 2
@pytest.mark.management
@pytest.mark.django_db
class TestRetaggerFileMoves:
def test_each_document_is_processed_in_its_own_scope(
self,
paperless_dirs: PaperlessDirs,
settings: Settings,
mocker: MockerFixture,
) -> None:
"""
GIVEN:
- Two documents and a filename format using the correspondent
- A classifier suggestion assigning a correspondent to each
WHEN:
- document_retagger runs with --correspondent
THEN:
- Each document is processed inside an open file move scope
- When the second document is processed, the first one's file has
already moved
- Both files end at their final paths
"""
settings.FILENAME_FORMAT = "{correspondent}/{title}"
(paperless_dirs.originals_dir / "none").mkdir(exist_ok=True)
for title in ("first", "second"):
(paperless_dirs.originals_dir / "none" / f"{title}.pdf").write_bytes(b"x")
DocumentFactory(
title=title,
filename=f"none/{title}.pdf",
mime_type="application/pdf",
)
correspondent = CorrespondentFactory(name="acme")
processed: list[Document] = []
scope_open: list[bool] = []
seen_first_moved: list[bool] = []
def fake_set_correspondent(sender, document, **kwargs):
scope_open.append(_active_collector.get() is not None)
# Order-agnostic: on the second call, check the first one's file.
if processed:
first = processed[0]
seen_first_moved.append(
(
paperless_dirs.originals_dir / "acme" / f"{first.title}.pdf"
).is_file(),
)
processed.append(document)
document.correspondent = correspondent
document.save(update_fields=("correspondent",))
return correspondent
mocker.patch(
"documents.management.commands.document_retagger.load_classifier",
)
mocker.patch(
"documents.management.commands.document_retagger.set_correspondent",
side_effect=fake_set_correspondent,
)
call_command("document_retagger", "--correspondent", skip_checks=True)
assert scope_open == [True, True]
assert seen_first_moved == [True]
for title in ("first", "second"):
assert (paperless_dirs.originals_dir / "acme" / f"{title}.pdf").is_file()
-121
View File
@@ -1,121 +0,0 @@
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import pytest
from django.db import transaction
from filelock import Timeout
from documents.file_moves import in_user_transaction
from documents.media_lock import media_lock
from paperless_testing.locks import record_media_lock_acquisitions
if TYPE_CHECKING:
from paperless_testing.dirs import PaperlessDirs
@pytest.mark.usefixtures("paperless_dirs")
class TestMediaLock:
def test_same_thread_reenters_without_blocking(self) -> None:
"""
GIVEN:
- media.lock held by the current thread
WHEN:
- The same thread acquires it again with a zero timeout
THEN:
- The nested acquisition succeeds immediately
"""
with media_lock(), media_lock(timeout=0):
pass
def test_other_thread_is_excluded(self) -> None:
"""
GIVEN:
- media.lock held by the current thread
WHEN:
- Another thread tries to acquire it with a short timeout
THEN:
- The other thread times out
- After release, the other thread can acquire it
"""
outcomes: list[str] = []
def try_acquire() -> None:
try:
with media_lock(timeout=0.2):
outcomes.append("acquired")
except Timeout:
outcomes.append("timeout")
with media_lock():
worker = threading.Thread(target=try_acquire)
worker.start()
worker.join()
worker = threading.Thread(target=try_acquire)
worker.start()
worker.join()
assert outcomes == ["timeout", "acquired"]
def test_lock_file_is_the_configured_path(
self,
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- The test's media directory layout
WHEN:
- media.lock is acquired
THEN:
- The lock file is created at settings.MEDIA_LOCK
"""
with media_lock():
assert paperless_dirs.media_lock.exists()
@pytest.mark.django_db
class TestInUserTransaction:
def test_false_inside_test_case_wrapper(self) -> None:
"""
GIVEN:
- The transaction pytest-django wraps around the test
WHEN:
- in_user_transaction is called
THEN:
- It returns False
"""
assert in_user_transaction() is False
def test_true_inside_atomic(self) -> None:
"""
GIVEN:
- An atomic block opened by code under test
WHEN:
- in_user_transaction is called inside it
THEN:
- It returns True
"""
with transaction.atomic():
assert in_user_transaction() is True
@pytest.mark.django_db
@pytest.mark.usefixtures("paperless_dirs")
class TestRecordMediaLockAcquisitions:
def test_records_only_first_acquisitions(self) -> None:
"""
GIVEN:
- Nested media.lock acquisitions, the outer one inside a transaction
WHEN:
- They are recorded
THEN:
- Only the outer, first acquisition is recorded, flagged as inside
a transaction
"""
with record_media_lock_acquisitions() as acquisitions:
with transaction.atomic(), media_lock(), media_lock():
pass
assert [a.in_user_transaction for a in acquisitions] == [True]
-101
View File
@@ -1,7 +1,4 @@
import pytest
from django.db import DatabaseError
from django.db import models
from django.db import transaction
from documents.models import Correspondent
from documents.models import Document
@@ -25,101 +22,3 @@ class TestDocument:
assert Correspondent.objects.count() == 0
assert Document.objects.count() == 1
assert Document.objects.first().correspondent is None
@pytest.mark.django_db
@pytest.mark.usefixtures("paperless_dirs")
class TestDocumentFilePathWrites:
def test_full_save_does_not_write_file_paths(self) -> None:
"""
GIVEN:
- A document whose filename columns were changed in the DB after
an instance was loaded
WHEN:
- The stale instance is saved without update_fields
THEN:
- The DB filename columns keep the newer values
- Other fields from the stale instance are written
"""
doc = DocumentFactory(filename="old.pdf", archive_filename="old.pdf")
stale = Document.objects.get(pk=doc.pk)
Document.objects.filter(pk=doc.pk).update(
filename="new.pdf",
archive_filename="new-archive.pdf",
)
stale.title = "changed"
stale.save()
doc.refresh_from_db()
assert doc.filename == "new.pdf"
assert doc.archive_filename == "new-archive.pdf"
assert doc.title == "changed"
@pytest.mark.parametrize(
"save_kwargs",
[
pytest.param({"write_file_paths": True}, id="write-file-paths"),
pytest.param(
{"update_fields": ["filename", "archive_filename"]},
id="explicit-update-fields",
),
],
)
def test_opt_in_writes_file_paths(self, save_kwargs: dict[str, object]) -> None:
"""
GIVEN:
- An existing document
WHEN:
- Its filename columns are changed in memory and saved with an
explicit opt in
THEN:
- The DB filename columns are updated
"""
doc = DocumentFactory(filename="old.pdf", archive_filename="old.pdf")
doc.filename = "new.pdf"
doc.archive_filename = "new.pdf"
doc.save(**save_kwargs)
doc.refresh_from_db()
assert doc.filename == "new.pdf"
assert doc.archive_filename == "new.pdf"
def test_creation_writes_file_paths(self) -> None:
"""
GIVEN:
- A new, unsaved document with filename columns set
WHEN:
- It is saved
THEN:
- The filename columns are written
"""
doc = DocumentFactory.build(filename="a.pdf", archive_filename="b.pdf")
doc.save()
doc.refresh_from_db()
assert doc.filename == "a.pdf"
assert doc.archive_filename == "b.pdf"
def test_full_save_of_deleted_row_raises(self) -> None:
"""
GIVEN:
- An instance whose row was hard-deleted elsewhere
WHEN:
- The instance is saved without update_fields
THEN:
- A DatabaseError is raised instead of re-inserting the row
"""
doc = DocumentFactory()
stale = Document.objects.get(pk=doc.pk)
# A plain QuerySet bypasses the soft-delete manager, so the row is
# really gone (as after emptying the trash).
models.QuerySet(model=Document).filter(pk=doc.pk).delete()
# The savepoint keeps the failed UPDATE from poisoning the test transaction.
with pytest.raises(DatabaseError), transaction.atomic():
stale.save()
assert not Document.global_objects.filter(pk=doc.pk).exists()
+2 -2
View File
@@ -132,7 +132,7 @@ class TestCheckSanityOriginal:
class TestCheckSanityArchive:
def test_checksum_without_filename(self, sample_doc: Document) -> None:
sample_doc.archive_filename = None
sample_doc.save(write_file_paths=True)
sample_doc.save()
messages = check_sanity()
assert messages.has_error
assert any(
@@ -185,7 +185,7 @@ class TestCheckSanityArchive:
Path(sample_doc.archive_path).unlink()
sample_doc.archive_checksum = None
sample_doc.archive_filename = None
sample_doc.save(write_file_paths=True)
sample_doc.save()
messages = check_sanity()
assert not messages.has_error
@@ -366,7 +366,7 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase):
def _write_document_file(self, *, archive: bool, content: bytes) -> Path:
if archive:
self.document.archive_filename = f"{self.document.pk:07}.pdf"
self.document.save(write_file_paths=True)
self.document.save()
path = self.document.archive_path
assert path is not None
else:
+62
View File
@@ -12,11 +12,73 @@ from documents.models import WorkflowTrigger
from documents.serialisers import TagSerializer
from documents.signals.handlers import run_workflows
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestTagHierarchyPermissions(APITestCase):
def test_delete_checks_descendant_permissions(self) -> None:
for mode in ("detail", "bulk", "all"):
for ownership in ("owned", "unowned", "private", "shared", "superuser"):
with self.subTest(mode=mode, ownership=ownership):
requester = UserFactory(superuser=ownership == "superuser")
grant_global(requester, "view_tag", "delete_tag")
self.client.force_authenticate(user=requester)
if ownership == "owned":
descendant_owner = requester
elif ownership == "unowned":
descendant_owner = None
else:
descendant_owner = UserFactory()
parent = TagFactory(owner=requester)
child = TagFactory(tn_parent=parent)
grandchild = TagFactory(
owner=descendant_owner,
tn_parent=child,
)
if ownership == "shared":
grant_object(requester, grandchild, "delete_tag")
document = DocumentFactory()
document.tags.add(grandchild)
ids = [parent.pk, child.pk, grandchild.pk]
if mode == "detail":
response = self.client.delete(f"/api/tags/{parent.pk}/")
else:
selection = (
{"all": True, "filters": {"id__in": str(parent.pk)}}
if mode == "all"
else {"objects": [parent.pk]}
)
response = self.client.post(
"/api/bulk_edit_objects/",
{"object_type": "tags", "operation": "delete", **selection},
format="json",
)
# Detail deletion accepts explicit delete grants; bulk
# deletion requires ownership (or an unowned object).
allowed = ownership in ("owned", "unowned", "superuser") or (
ownership == "shared" and mode == "detail"
)
expected_status = status.HTTP_403_FORBIDDEN
if allowed:
expected_status = (
status.HTTP_204_NO_CONTENT
if mode == "detail"
else status.HTTP_200_OK
)
assert response.status_code == expected_status
assert Tag.objects.filter(pk__in=ids).count() == (
0 if allowed else 3
)
assert document.tags.filter(pk=grandchild.pk).exists() == (
not allowed
)
def test_children_only_include_visible_tags(self) -> None:
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
+20 -3
View File
@@ -126,7 +126,6 @@ from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
from documents.file_handling import format_filename
from documents.file_moves import flush_pending_file_moves
from documents.filters import CorrespondentFilterSet
from documents.filters import CustomFieldFilterSet
from documents.filters import DocumentFilterSet
@@ -577,6 +576,15 @@ class CorrespondentViewSet(
return super().retrieve(request, *args, **kwargs)
def _with_tag_descendants(tags: QuerySet[Tag]) -> QuerySet[Tag]:
"""Include every tag that deleting the selected tags will affect."""
tag_ids = set()
for tag in tags:
tag_ids.add(tag.pk)
tag_ids.update(tag.get_descendants_pks())
return Tag.objects.filter(pk__in=tag_ids)
@extend_schema_view(**generate_object_with_permissions_schema(TagSerializer))
class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Tag]):
model = Tag
@@ -598,6 +606,14 @@ class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Tag]):
filterset_class = TagFilterSet
ordering_fields = ("color", "name", "matching_algorithm", "match", "document_count")
def perform_destroy(self, instance):
tags = _with_tag_descendants(Tag.objects.filter(pk=instance.pk))
if tags.exclude(
pk__in=permitted_object_ids(self.request.user, Tag, "delete_tag"),
).exists():
raise PermissionDenied(_("Insufficient permissions."))
super().perform_destroy(instance)
def get_serializer_context(self):
context = super().get_serializer_context()
context["document_count_filter"] = self.get_document_count_filter()
@@ -1265,8 +1281,6 @@ class DocumentViewSet(
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
# The response must reflect the post-move filename and modified time
flush_pending_file_moves()
if content_updated and content_doc.id != root_doc.id:
content_doc.content = (
@@ -5078,6 +5092,9 @@ class BulkEditObjectsView(PassUserMixin):
pk__in=object_ids,
)
if object_type == "tags" and operation == "delete":
objs = _with_tag_descendants(objs)
if not user.is_superuser:
perm = f"documents.{perm_codename}"
# Limited to the owner (or unowned), same as documents, see BulkEditView
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Ongeldige veranderlike bespeur."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Amharic\n"
"Language: am_ET\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "اكتشاف متغير خاطئ."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Belarusian\n"
"Language: be_BY\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Выяўлена няправільная зменная."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "стартиране на работните процеси"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Засечена е невалидна променлива."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Language: ca_ES\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "flux corrents"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Permisos insuficients."
@@ -1680,7 +1680,7 @@ msgstr "Variable detectada invàlida."
msgid "Duplicate document identifiers are not allowed."
msgstr "Duplicat d'identificadors de documents no permès."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Documents no trobats: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ""
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "Invalid more_like_id"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Configuració AI invàlida."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Permisos insuficients per compartir document %(id)s."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Paquet ja s'està processant."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "El paquet d'enllaç no està disponible."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "spuštění pracovních postupů"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Nedostatečná oprávnění."
@@ -1680,7 +1680,7 @@ msgstr "Zjištěna neplatná proměnná."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Nedostatečná oprávnění ke sdílení dokumentu %(id)s."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Language: da_DK\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "workflow-kørsler"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Ugyldig variabel fundet."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: German, Switzerland\n"
"Language: de_CH\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "Arbeitsablauf wird ausgeführt"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen."
@@ -1680,7 +1680,7 @@ msgstr "Ungültige Variable erkannt."
msgid "Duplicate document identifiers are not allowed."
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Dokumente nicht gefunden: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ""
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "Ungültige more_like_id"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Ungültige KI-Konfiguration."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-11-03 00:31\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "Arbeitsablauf wird ausgeführt"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen."
@@ -1680,7 +1680,7 @@ msgstr "Ungültige Variable erkannt."
msgid "Duplicate document identifiers are not allowed."
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Dokumente nicht gefunden: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ", "
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "Ungültige more_like_id"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Ungültige KI-Konfiguration."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr "Zeitüberschreitung bei der KI-Backendanfrage."
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr "Das KI-Backend hat die Anfrage abgelehnt. Details in den Protokollen."
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "εκτελέσεις ροής εργασίας"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Εντοπίστηκε μη έγκυρη μεταβλητή."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+12 -12
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1657,8 +1657,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1698,7 +1698,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1953,36 +1953,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "ejecuciones de flujo de trabajo"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Permisos insuficientes."
@@ -1680,7 +1680,7 @@ msgstr "Variable inválida."
msgid "Duplicate document identifiers are not allowed."
msgstr "No se permiten identificadores de documento duplicados."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Documentos no encontrados: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ""
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Configuración de IA inválida."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Permisos insuficientes para compartir el documento %(id)s."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "El paquete ya está siendo procesado."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "El paquete de enlace compartido no está disponible."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Estonian\n"
"Language: et_EE\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Language: fa_IR\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "گردش کار اجرا می شود"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "متغیر نامعتبر شناسایی شده است."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Language: fi_FI\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Virheellinen muuttuja havaittu."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "le flux de travail s'exécute"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Droits insuffisants."
@@ -1680,7 +1680,7 @@ msgstr "Variable invalide détectée."
msgid "Duplicate document identifiers are not allowed."
msgstr "Les identificateurs de document en double ne sont pas autorisés."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Documents introuvables : %(ids)s"
@@ -1924,36 +1924,36 @@ msgstr ""
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "More_like_id invalide"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Configuration IA invalide."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr "La requête d'arrière-plan IA a expiré."
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Droits d'accès insuffisant pour partager %(id)s document."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Le paquet est déjà en cours de traitement."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "Le lot de liens de partage n'est pas disponible."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Language: he_IL\n"
@@ -1641,8 +1641,8 @@ msgid "workflow runs"
msgstr "הרצות זרימת עבודה"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "הרשאות אינן מספיקות."
@@ -1681,7 +1681,7 @@ msgstr "משתנה לא חוקי זוהה."
msgid "Duplicate document identifiers are not allowed."
msgstr "מזהי מסמכים כפולים אינם מורשים."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "מסמכים לא נמצאו: %(ids)s"
@@ -1926,36 +1926,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "יש לציין רק אחד מהבאים: text‏, title_search‏, query או more_like_id."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "מזהה more_like_id אינו תקין"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "הגדרות בינה מלאכותית שגויות."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "הרשאות לא מספיקות לשיתוף מסמך %(id)s."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "החבילה (Bundle) כבר נמצאת בתהליך עיבוד."
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "חבילת קישור השיתוף עדיין בהכנה. נא לנסות שוב מאוחר יותר."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "חבילת קישור השיתוף אינה זמינה."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Language: hi_IN\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "tijek rada pokrenut"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Nedovoljne ovlasti."
@@ -1680,7 +1680,7 @@ msgstr "Otkrivena je nevaljana vrsta datoteke."
msgid "Duplicate document identifiers are not allowed."
msgstr "Duplicirani identifikatori dokumenata nisu dopušteni."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Dokumenti nisu pronađeni: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ""
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "Nevažeći more_like_id"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Nevažeća AI konfiguracija."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr "Zahtjev prema AI pozadinskom sustavu je istekao."
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr "AI pozadinski sustav je odbio zahtjev. Provjerite zapise (logs) za više detalja."
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Nedovoljne ovlasti za dijeljenje dokumenta %(id)s."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Paket se već obrađuje."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "Paket linka za dijeljenje nije dostupan."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Language: hu_HU\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "munkafolyamat futtatások"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Nincs jogosúltsága."
@@ -1680,7 +1680,7 @@ msgstr "Érvénytelen változó észlelve."
msgid "Duplicate document identifiers are not allowed."
msgstr "A dokumentumazonosítók duplikálása nem megengedett."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Dokumentumok nem találhatók: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ""
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "Érvénytelen more_like_id"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Érvénytelen MI konfiguráció."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Nincs megfelelő jogosultság a %(id)s dokumentum megosztásához."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "A csomag feldolgozása már folyamatban van."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "A megosztási linkcsomag nem elérhető."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Indonesian\n"
"Language: id_ID\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "daftar jalankan alur kerja"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Izin tidak mencukupi"
@@ -1680,7 +1680,7 @@ msgstr "Variabel ilegal terdeteksi."
msgid "Duplicate document identifiers are not allowed."
msgstr "Penggunaan pengenal dokumen ganda tidak diperbolehkan."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Dokumen tidak ditemukan: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Izin tidak mencukupi untuk berbagi dokumen %(id)s"
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Paket sedang diproses."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "Bundel tautan berbagi tidak tersedia."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "esecuzioni del flusso di lavoro"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr "Autorizzazioni insufficienti."
@@ -1680,7 +1680,7 @@ msgstr "Variabile non valida rilevata."
msgid "Duplicate document identifiers are not allowed."
msgstr "Non sono consentiti identificatori di documenti duplicati."
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr "Documenti non trovati: %(ids)s"
@@ -1925,36 +1925,36 @@ msgstr ", "
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."
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr "more_like_id non valido"
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr "Configurazione AI non valida."
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr "Richiesta di backend AI scaduta."
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr "Il backend IA ha rifiutato la richiesta. Controlla i log per i dettagli."
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr "Autorizzazioni insufficienti per condividere il documento %(id)s."
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr "Il pacchetto è già in fase di elaborazione."
#: documents/views.py:4910
#: documents/views.py:4927
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."
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr "Il pacchetto di link di condivisione non è disponibile."
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "ワークフローの実行"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "無効な変数を検出しました"
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr "워크플로우 실행"
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "잘못된 변수가 감지되었습니다."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1925,36 +1925,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
+13 -13
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-31 16:33+0000\n"
"PO-Revision-Date: 2026-10-31 16:34\n"
"POT-Creation-Date: 2026-11-04 19:06+0000\n"
"PO-Revision-Date: 2026-11-04 19:07\n"
"Last-Translator: \n"
"Language-Team: Luxembourgish\n"
"Language: lb_LU\n"
@@ -1640,8 +1640,8 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:2731
#: paperless_mail/serialisers.py:156
#: documents/serialisers.py:2872 documents/views.py:348 documents/views.py:614
#: documents/views.py:2748 paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1680,7 +1680,7 @@ msgstr "Ongëlteg Zeechen detektéiert."
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4781
#: documents/serialisers.py:2958 documents/views.py:4798
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1924,36 +1924,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:341 documents/views.py:2728
#: documents/views.py:341 documents/views.py:2745
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1652
#: documents/views.py:1669
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1663
#: documents/views.py:1680
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1675
#: documents/views.py:1692
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4797
#: documents/views.py:4814
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4844
#: documents/views.py:4861
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4910
#: documents/views.py:4927
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4924
#: documents/views.py:4941
msgid "The share link bundle is unavailable."
msgstr ""
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff. Show more