mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-24 00:59:35 -06:00
Compare commits
16 Commits
feature-82
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9ee564673 | ||
|
|
fa13ca7a42 | ||
|
|
814f57b099 | ||
|
|
be7f1c6233 | ||
|
|
d6cd6d0311 | ||
|
|
095ea3cbd3 | ||
|
|
5b667621cd | ||
|
|
1b912c137a | ||
|
|
98298e37cd | ||
|
|
35be0850ec | ||
|
|
1bb4b9b473 | ||
|
|
f85094dc2b | ||
|
|
65ca78e9e7 | ||
|
|
57c5939d7b | ||
|
|
43fe932c57 | ||
|
|
83f68d6063 |
2
.github/workflows/ci-backend.yml
vendored
2
.github/workflows/ci-backend.yml
vendored
@@ -129,6 +129,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv pip list
|
uv pip list
|
||||||
- name: Check typing (pyrefly)
|
- name: Check typing (pyrefly)
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
uv run pyrefly \
|
uv run pyrefly \
|
||||||
check \
|
check \
|
||||||
@@ -143,6 +144,7 @@ jobs:
|
|||||||
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
||||||
${{ runner.os }}-mypy-
|
${{ runner.os }}-mypy-
|
||||||
- name: Check typing (mypy)
|
- name: Check typing (mypy)
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
uv run mypy \
|
uv run mypy \
|
||||||
--show-error-codes \
|
--show-error-codes \
|
||||||
|
|||||||
@@ -2192,34 +2192,34 @@ src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "
|
|||||||
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "flagged" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "flagged" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "seen" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "seen" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "seen" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "seen" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[att@480]" has no attribute "filename" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[att@481]" has no attribute "filename" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@426]" has no attribute "from_" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@427]" has no attribute "from_" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@426]" has no attribute "from_" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@427]" has no attribute "from_" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@426]" has no attribute "from_values" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@427]" has no attribute "from_values" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message@419]" has no attribute "from_" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message@420]" has no attribute "from_" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message@419]" has no attribute "from_values" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message@420]" has no attribute "from_values" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message@478]" has no attribute "subject" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message@479]" has no attribute "subject" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: "type[message@531]" has no attribute "attachments" [attr-defined]
|
src/paperless_mail/tests/test_mail.py:0: error: "type[message@532]" has no attribute "attachments" [attr-defined]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxFolderSelectError" has incompatible type "None"; expected "tuple[Any, ...]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxFolderSelectError" has incompatible type "None"; expected "tuple[Any, ...]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxFolderSelectError" has incompatible type "None"; expected "tuple[Any, ...]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxFolderSelectError" has incompatible type "None"; expected "tuple[Any, ...]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@426]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@427]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@426]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@427]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@478]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@479]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@478]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@479]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@478]"; expected "MailMessage" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@479]"; expected "MailMessage" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[MailMessage], TypeGuard[Never]]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[MailMessage], TypeGuard[Never]]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[MailMessage], TypeGuard[Never]]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[MailMessage], TypeGuard[Never]]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@480]"; expected "MailAttachment" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@481]"; expected "MailAttachment" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@480]"; expected "MailAttachment" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@481]"; expected "MailAttachment" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@480]"; expected "MailAttachment" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@481]"; expected "MailAttachment" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "assertIn" of "TestCase" has incompatible type "str | None"; expected "Iterable[Any] | Container[Any]" [arg-type]
|
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "assertIn" of "TestCase" has incompatible type "str | None"; expected "Iterable[Any] | Container[Any]" [arg-type]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Dict entry 0 has incompatible type "str": "None"; expected "str": "str" [dict-item]
|
src/paperless_mail/tests/test_mail.py:0: error: Dict entry 0 has incompatible type "str": "None"; expected "str": "str" [dict-item]
|
||||||
src/paperless_mail/tests/test_mail.py:0: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" [dict-item]
|
src/paperless_mail/tests/test_mail.py:0: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" [dict-item]
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.10.0-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.10.5-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -784,9 +784,17 @@ below.
|
|||||||
|
|
||||||
### Document Splitting {#document-splitting}
|
### Document Splitting {#document-splitting}
|
||||||
|
|
||||||
When enabled, Paperless will look for a barcode with the configured value and create a new document
|
If document splitting is enabled, Paperless splits _after_ a separator barcode by default.
|
||||||
starting from the next page. The page with the barcode on it will _not_ be retained. It
|
This means:
|
||||||
is expected to be a page existing only for triggering the split.
|
|
||||||
|
- any page containing the configured separator barcode starts a new document, starting with the **next** page
|
||||||
|
- pages containing the separator barcode are discarded
|
||||||
|
|
||||||
|
This is intended for dedicated separator sheets such as PATCH-T pages.
|
||||||
|
|
||||||
|
If [`PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES`](configuration.md#PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES)
|
||||||
|
is enabled, the page containing the separator barcode is retained instead. In this mode,
|
||||||
|
each page containing the separator barcode becomes the **first** page of a new document.
|
||||||
|
|
||||||
### Archive Serial Number Assignment
|
### Archive Serial Number Assignment
|
||||||
|
|
||||||
@@ -795,8 +803,9 @@ archive serial number, allowing quick reference back to the original, paper docu
|
|||||||
|
|
||||||
If document splitting via barcode is also enabled, documents will be split when an ASN
|
If document splitting via barcode is also enabled, documents will be split when an ASN
|
||||||
barcode is located. However, differing from the splitting, the page with the
|
barcode is located. However, differing from the splitting, the page with the
|
||||||
barcode _will_ be retained. This allows application of a barcode to any page, including
|
barcode _will_ be retained. Each detected ASN barcode starts a new document _starting with
|
||||||
one which holds data to keep in the document.
|
that page_. This allows placing ASN barcodes on content pages that should remain part of
|
||||||
|
the document.
|
||||||
|
|
||||||
### Tag Assignment
|
### Tag Assignment
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.8
|
||||||
|
|
||||||
## paperless-ngx 2.20.7
|
## paperless-ngx 2.20.7
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.20.7"
|
version = "2.20.8"
|
||||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -1781,11 +1781,15 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">216</context>
|
<context context-type="linenumber">156</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">241</context>
|
<context context-type="linenumber">230</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
|
<context context-type="linenumber">255</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2991443309752293110" datatype="html">
|
<trans-unit id="2991443309752293110" datatype="html">
|
||||||
@@ -3113,21 +3117,21 @@
|
|||||||
<source>Sidebar views updated</source>
|
<source>Sidebar views updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">329</context>
|
<context context-type="linenumber">343</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3547923076537026828" datatype="html">
|
<trans-unit id="3547923076537026828" datatype="html">
|
||||||
<source>Error updating sidebar views</source>
|
<source>Error updating sidebar views</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">332</context>
|
<context context-type="linenumber">346</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2526035785704676448" datatype="html">
|
<trans-unit id="2526035785704676448" datatype="html">
|
||||||
<source>An error occurred while saving update checking settings.</source>
|
<source>An error occurred while saving update checking settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">353</context>
|
<context context-type="linenumber">367</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4580988005648117665" datatype="html">
|
<trans-unit id="4580988005648117665" datatype="html">
|
||||||
@@ -8486,7 +8490,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">315</context>
|
<context context-type="linenumber">323</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
||||||
@@ -8501,7 +8505,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">308</context>
|
<context context-type="linenumber">316</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.html</context>
|
||||||
@@ -8767,49 +8771,49 @@
|
|||||||
<source>Reset filters / selection</source>
|
<source>Reset filters / selection</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">296</context>
|
<context context-type="linenumber">304</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4135055128446167640" datatype="html">
|
<trans-unit id="4135055128446167640" datatype="html">
|
||||||
<source>Open first [selected] document</source>
|
<source>Open first [selected] document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">324</context>
|
<context context-type="linenumber">332</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3629960544875360046" datatype="html">
|
<trans-unit id="3629960544875360046" datatype="html">
|
||||||
<source>Previous page</source>
|
<source>Previous page</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">340</context>
|
<context context-type="linenumber">348</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3337301694210287595" datatype="html">
|
<trans-unit id="3337301694210287595" datatype="html">
|
||||||
<source>Next page</source>
|
<source>Next page</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">352</context>
|
<context context-type="linenumber">360</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2155249406916744630" datatype="html">
|
<trans-unit id="2155249406916744630" datatype="html">
|
||||||
<source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source>
|
<source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">385</context>
|
<context context-type="linenumber">393</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4646273665293421938" datatype="html">
|
<trans-unit id="4646273665293421938" datatype="html">
|
||||||
<source>Failed to save view "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>".</source>
|
<source>Failed to save view "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>".</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">391</context>
|
<context context-type="linenumber">399</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6837554170707123455" datatype="html">
|
<trans-unit id="6837554170707123455" datatype="html">
|
||||||
<source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source>
|
<source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
<context context-type="linenumber">437</context>
|
<context context-type="linenumber">445</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="739880801667335279" datatype="html">
|
<trans-unit id="739880801667335279" datatype="html">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "2.20.7",
|
"version": "2.20.8",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -243,9 +243,19 @@ describe('AppFrameComponent', () => {
|
|||||||
|
|
||||||
it('should support toggling slim sidebar and saving', fakeAsync(() => {
|
it('should support toggling slim sidebar and saving', fakeAsync(() => {
|
||||||
const saveSettingSpy = jest.spyOn(settingsService, 'set')
|
const saveSettingSpy = jest.spyOn(settingsService, 'set')
|
||||||
|
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
|
||||||
expect(component.slimSidebarEnabled).toBeFalsy()
|
expect(component.slimSidebarEnabled).toBeFalsy()
|
||||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||||
component.toggleSlimSidebar()
|
component.toggleSlimSidebar()
|
||||||
|
const requests = httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}ui_settings/`
|
||||||
|
)
|
||||||
|
expect(requests).toHaveLength(1)
|
||||||
|
expect(requests[0].request.body.settings.slim_sidebar).toBe(true)
|
||||||
|
expect(
|
||||||
|
requests[0].request.body.settings.attributes_sections_collapsed
|
||||||
|
).toEqual(['attributes'])
|
||||||
|
requests[0].flush({ success: true })
|
||||||
expect(component.slimSidebarAnimating).toBeTruthy()
|
expect(component.slimSidebarAnimating).toBeTruthy()
|
||||||
tick(200)
|
tick(200)
|
||||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||||
@@ -254,6 +264,10 @@ describe('AppFrameComponent', () => {
|
|||||||
SETTINGS_KEYS.SLIM_SIDEBAR,
|
SETTINGS_KEYS.SLIM_SIDEBAR,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
expect(saveSettingSpy).toHaveBeenCalledWith(
|
||||||
|
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
['attributes']
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
it('should show error on toggle slim sidebar if store settings fails', () => {
|
it('should show error on toggle slim sidebar if store settings fails', () => {
|
||||||
|
|||||||
@@ -140,10 +140,24 @@ export class AppFrameComponent
|
|||||||
|
|
||||||
toggleSlimSidebar(): void {
|
toggleSlimSidebar(): void {
|
||||||
this.slimSidebarAnimating = true
|
this.slimSidebarAnimating = true
|
||||||
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
const slimSidebarEnabled = !this.slimSidebarEnabled
|
||||||
if (this.slimSidebarEnabled) {
|
this.settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, slimSidebarEnabled)
|
||||||
this.attributesSectionsCollapsed = true
|
if (slimSidebarEnabled) {
|
||||||
|
this.settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [
|
||||||
|
CollapsibleSection.ATTRIBUTES,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
this.settingsService
|
||||||
|
.storeSettings()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred while saving settings.`
|
||||||
|
)
|
||||||
|
console.warn(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.slimSidebarAnimating = false
|
this.slimSidebarAnimating = false
|
||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
||||||
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||||
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -147,21 +147,21 @@ describe('DocumentListComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show score sort fields on fulltext queries', () => {
|
it('should show score sort fields on fulltext queries', () => {
|
||||||
documentListService.filterRules = [
|
documentListService.setFilterRules([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_HAS_TAGS_ANY,
|
rule_type: FILTER_HAS_TAGS_ANY,
|
||||||
value: '10',
|
value: '10',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.getSortFields()).toEqual(documentListService.sortFields)
|
expect(component.getSortFields()).toEqual(documentListService.sortFields)
|
||||||
|
|
||||||
documentListService.filterRules = [
|
documentListService.setFilterRules([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_FULLTEXT_QUERY,
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
value: 'foo',
|
value: 'foo',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.getSortFields()).toEqual(
|
expect(component.getSortFields()).toEqual(
|
||||||
documentListService.sortFieldsFullText
|
documentListService.sortFieldsFullText
|
||||||
@@ -170,12 +170,12 @@ describe('DocumentListComponent', () => {
|
|||||||
|
|
||||||
it('should determine if filtered, support reset', () => {
|
it('should determine if filtered, support reset', () => {
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
documentListService.filterRules = [
|
documentListService.setFilterRules([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_HAS_TAGS_ANY,
|
rule_type: FILTER_HAS_TAGS_ANY,
|
||||||
value: '10',
|
value: '10',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
documentListService.isReloading = false
|
documentListService.isReloading = false
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.isFiltered).toBeTruthy()
|
expect(component.isFiltered).toBeTruthy()
|
||||||
@@ -185,6 +185,20 @@ describe('DocumentListComponent', () => {
|
|||||||
expect(fixture.nativeElement.textContent.match(/Reset/g)).toHaveLength(1)
|
expect(fixture.nativeElement.textContent.match(/Reset/g)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should apply filter rule changes via list service', () => {
|
||||||
|
const setFilterRulesSpy = jest.spyOn(documentListService, 'setFilterRules')
|
||||||
|
const rules = [{ rule_type: FILTER_HAS_TAGS_ANY, value: '10' }]
|
||||||
|
component.onFilterRulesChange(rules)
|
||||||
|
expect(setFilterRulesSpy).toHaveBeenCalledWith(rules)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset filter rules to page one via list service', () => {
|
||||||
|
const setFilterRulesSpy = jest.spyOn(documentListService, 'setFilterRules')
|
||||||
|
const rules = [{ rule_type: FILTER_HAS_TAGS_ANY, value: '10' }]
|
||||||
|
component.onFilterRulesReset(rules)
|
||||||
|
expect(setFilterRulesSpy).toHaveBeenCalledWith(rules, true)
|
||||||
|
})
|
||||||
|
|
||||||
it('should load saved view from URL', () => {
|
it('should load saved view from URL', () => {
|
||||||
const view: SavedView = {
|
const view: SavedView = {
|
||||||
id: 10,
|
id: 10,
|
||||||
@@ -217,7 +231,7 @@ describe('DocumentListComponent', () => {
|
|||||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||||
.mockReturnValue(of(convertToParamMap(queryParams)))
|
.mockReturnValue(of(convertToParamMap(queryParams)))
|
||||||
activatedRoute.snapshot.queryParams = queryParams
|
activatedRoute.snapshot.queryParams = queryParams
|
||||||
fixture.detectChanges()
|
component.ngOnInit()
|
||||||
expect(getSavedViewSpy).toHaveBeenCalledWith(view.id)
|
expect(getSavedViewSpy).toHaveBeenCalledWith(view.id)
|
||||||
expect(activateSavedViewSpy).toHaveBeenCalledWith(
|
expect(activateSavedViewSpy).toHaveBeenCalledWith(
|
||||||
view,
|
view,
|
||||||
|
|||||||
@@ -212,6 +212,14 @@ export class DocumentListComponent
|
|||||||
this.list.setSort(event.column, event.reverse)
|
this.list.setSort(event.column, event.reverse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFilterRulesChange(filterRules: FilterRule[]) {
|
||||||
|
this.list.setFilterRules(filterRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterRulesReset(filterRules: FilterRule[]) {
|
||||||
|
this.list.setFilterRules(filterRules, true)
|
||||||
|
}
|
||||||
|
|
||||||
get isBulkEditing(): boolean {
|
get isBulkEditing(): boolean {
|
||||||
return this.list.selected.size > 0
|
return this.list.selected.size > 0
|
||||||
}
|
}
|
||||||
@@ -300,7 +308,7 @@ export class DocumentListComponent
|
|||||||
if (this.list.selected.size > 0) {
|
if (this.list.selected.size > 0) {
|
||||||
this.list.selectNone()
|
this.list.selectNone()
|
||||||
} else if (this.isFiltered) {
|
} else if (this.isFiltered) {
|
||||||
this.filterEditor.resetSelected()
|
this.resetFilters()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2107,6 +2107,22 @@ describe('FilterEditorComponent', () => {
|
|||||||
expect(component.filterRules).toEqual(rules)
|
expect(component.filterRules).toEqual(rules)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should emit reset filter rules when resetting', () => {
|
||||||
|
const rules = [{ rule_type: FILTER_HAS_TAGS_ANY, value: '2' }]
|
||||||
|
component.unmodifiedFilterRules = rules
|
||||||
|
component.filterRules = [
|
||||||
|
{ rule_type: FILTER_DOES_NOT_HAVE_TAG, value: '2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const resetFilterRulesSpy = jest.spyOn(component.resetFilterRules, 'next')
|
||||||
|
const filterRulesChangeSpy = jest.spyOn(component.filterRulesChange, 'next')
|
||||||
|
|
||||||
|
component.resetSelected()
|
||||||
|
|
||||||
|
expect(resetFilterRulesSpy).toHaveBeenCalledWith(rules)
|
||||||
|
expect(filterRulesChangeSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('should support resetting text field', () => {
|
it('should support resetting text field', () => {
|
||||||
component.textFilter = 'foo'
|
component.textFilter = 'foo'
|
||||||
component.resetTextField()
|
component.resetTextField()
|
||||||
|
|||||||
@@ -1101,6 +1101,9 @@ export class FilterEditorComponent
|
|||||||
@Output()
|
@Output()
|
||||||
filterRulesChange = new EventEmitter<FilterRule[]>()
|
filterRulesChange = new EventEmitter<FilterRule[]>()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
resetFilterRules = new EventEmitter<FilterRule[]>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selectionData(selectionData: SelectionData) {
|
set selectionData(selectionData: SelectionData) {
|
||||||
this.tagDocumentCounts = selectionData?.selected_tags ?? null
|
this.tagDocumentCounts = selectionData?.selected_tags ?? null
|
||||||
@@ -1244,7 +1247,7 @@ export class FilterEditorComponent
|
|||||||
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||||
this.documentService.searchQuery = ''
|
this.documentService.searchQuery = ''
|
||||||
this.filterRules = this._unmodifiedFilterRules
|
this.filterRules = this._unmodifiedFilterRules
|
||||||
this.updateRules()
|
this.resetFilterRules.next(this.filterRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTag(tagId: number) {
|
toggleTag(tagId: number) {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ describe('DocumentListViewService', () => {
|
|||||||
value: tags__id__in,
|
value: tags__id__in,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
documentListViewService.filterRules = filterRulesAny
|
documentListViewService.setFilterRules(filterRulesAny)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
||||||
)
|
)
|
||||||
@@ -178,7 +178,7 @@ describe('DocumentListViewService', () => {
|
|||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -210,7 +210,7 @@ describe('DocumentListViewService', () => {
|
|||||||
value: tags__id__in,
|
value: tags__id__in,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
documentListViewService.filterRules = filterRulesAny
|
documentListViewService.setFilterRules(filterRulesAny)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
||||||
)
|
)
|
||||||
@@ -218,7 +218,7 @@ describe('DocumentListViewService', () => {
|
|||||||
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
|
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
|
||||||
expect(documentListViewService.error).toEqual('Generic error')
|
expect(documentListViewService.error).toEqual('Generic error')
|
||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -295,13 +295,41 @@ describe('DocumentListViewService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should use filter rules to update query params', () => {
|
it('should use filter rules to update query params', () => {
|
||||||
documentListViewService.filterRules = filterRules
|
documentListViewService.setFilterRules(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support setting filter rules and resetting to page one', () => {
|
||||||
|
documentListViewService.currentPage = 2
|
||||||
|
let req = httpTestingController.expectOne((request) =>
|
||||||
|
request.urlWithParams.startsWith(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('GET')
|
||||||
|
req.flush(full_results)
|
||||||
|
req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/selection_data/`
|
||||||
|
)
|
||||||
|
req.flush([])
|
||||||
|
|
||||||
|
documentListViewService.setFilterRules(filterRules, true)
|
||||||
|
|
||||||
|
const filteredReqs = httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||||
|
)
|
||||||
|
expect(filteredReqs).toHaveLength(1)
|
||||||
|
filteredReqs[0].flush(full_results)
|
||||||
|
req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/selection_data/`
|
||||||
|
)
|
||||||
|
req.flush([])
|
||||||
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should support quick filter', () => {
|
it('should support quick filter', () => {
|
||||||
documentListViewService.quickFilter(filterRules)
|
documentListViewService.quickFilter(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
@@ -336,7 +364,7 @@ describe('DocumentListViewService', () => {
|
|||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -348,7 +376,7 @@ describe('DocumentListViewService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support navigating next / previous', () => {
|
it('should support navigating next / previous', () => {
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@@ -558,7 +586,7 @@ describe('DocumentListViewService', () => {
|
|||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
expect(documentListViewService.selected.size).toEqual(6)
|
expect(documentListViewService.selected.size).toEqual(6)
|
||||||
|
|
||||||
documentListViewService.filterRules = filterRules
|
documentListViewService.setFilterRules(filterRules)
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
@@ -592,7 +620,7 @@ describe('DocumentListViewService', () => {
|
|||||||
|
|
||||||
documentListViewService.loadSavedView(view2)
|
documentListViewService.loadSavedView(view2)
|
||||||
expect(documentListViewService.sortField).toEqual('score')
|
expect(documentListViewService.sortField).toEqual('score')
|
||||||
documentListViewService.filterRules = []
|
documentListViewService.setFilterRules([])
|
||||||
expect(documentListViewService.sortField).toEqual('created')
|
expect(documentListViewService.sortField).toEqual('created')
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export class DocumentListViewService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
set filterRules(filterRules: FilterRule[]) {
|
setFilterRules(filterRules: FilterRule[], resetPage: boolean = false) {
|
||||||
if (
|
if (
|
||||||
!isFullTextFilterRule(filterRules) &&
|
!isFullTextFilterRule(filterRules) &&
|
||||||
this.activeListViewState.sortField == 'score'
|
this.activeListViewState.sortField == 'score'
|
||||||
@@ -350,6 +350,9 @@ export class DocumentListViewService {
|
|||||||
this.activeListViewState.sortField = 'created'
|
this.activeListViewState.sortField = 'created'
|
||||||
}
|
}
|
||||||
this.activeListViewState.filterRules = filterRules
|
this.activeListViewState.filterRules = filterRules
|
||||||
|
if (resetPage) {
|
||||||
|
this.activeListViewState.currentPage = 1
|
||||||
|
}
|
||||||
this.reload()
|
this.reload()
|
||||||
this.reduceSelectionToFilter()
|
this.reduceSelectionToFilter()
|
||||||
this.saveDocumentListView()
|
this.saveDocumentListView()
|
||||||
@@ -479,7 +482,7 @@ export class DocumentListViewService {
|
|||||||
|
|
||||||
quickFilter(filterRules: FilterRule[]) {
|
quickFilter(filterRules: FilterRule[]) {
|
||||||
this._activeSavedViewId = null
|
this._activeSavedViewId = null
|
||||||
this.filterRules = filterRules
|
this.setFilterRules(filterRules)
|
||||||
this.router.navigate(['documents'])
|
this.router.navigate(['documents'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const environment = {
|
|||||||
apiVersion: '9', // match src/paperless/settings.py
|
apiVersion: '9', // match src/paperless/settings.py
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
tag: 'prod',
|
tag: 'prod',
|
||||||
version: '2.20.7',
|
version: '2.20.8',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[tuple[int, int, int]] = (2, 20, 7)
|
__version__: Final[tuple[int, int, int]] = (2, 20, 8)
|
||||||
# Version string like X.Y.Z
|
# Version string like X.Y.Z
|
||||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||||
# Version string like X.Y
|
# Version string like X.Y
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def get_embedding_model() -> BaseEmbedding:
|
|||||||
return OpenAIEmbedding(
|
return OpenAIEmbedding(
|
||||||
model=config.llm_embedding_model or "text-embedding-3-small",
|
model=config.llm_embedding_model or "text-embedding-3-small",
|
||||||
api_key=config.llm_api_key,
|
api_key=config.llm_api_key,
|
||||||
|
api_base=config.llm_endpoint or None,
|
||||||
)
|
)
|
||||||
case LLMEmbeddingBackend.HUGGINGFACE:
|
case LLMEmbeddingBackend.HUGGINGFACE:
|
||||||
return HuggingFaceEmbedding(
|
return HuggingFaceEmbedding(
|
||||||
|
|||||||
@@ -65,12 +65,14 @@ def test_get_embedding_model_openai(mock_ai_config):
|
|||||||
mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.OPENAI
|
mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.OPENAI
|
||||||
mock_ai_config.return_value.llm_embedding_model = "text-embedding-3-small"
|
mock_ai_config.return_value.llm_embedding_model = "text-embedding-3-small"
|
||||||
mock_ai_config.return_value.llm_api_key = "test_api_key"
|
mock_ai_config.return_value.llm_api_key = "test_api_key"
|
||||||
|
mock_ai_config.return_value.llm_endpoint = "http://test-url"
|
||||||
|
|
||||||
with patch("paperless_ai.embedding.OpenAIEmbedding") as MockOpenAIEmbedding:
|
with patch("paperless_ai.embedding.OpenAIEmbedding") as MockOpenAIEmbedding:
|
||||||
model = get_embedding_model()
|
model = get_embedding_model()
|
||||||
MockOpenAIEmbedding.assert_called_once_with(
|
MockOpenAIEmbedding.assert_called_once_with(
|
||||||
model="text-embedding-3-small",
|
model="text-embedding-3-small",
|
||||||
api_key="test_api_key",
|
api_key="test_api_key",
|
||||||
|
api_base="http://test-url",
|
||||||
)
|
)
|
||||||
assert model == MockOpenAIEmbedding.return_value
|
assert model == MockOpenAIEmbedding.return_value
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
name="stop_processing",
|
name="stop_processing",
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text="If True, no further rules will be processed after this one if any document is consumed.",
|
help_text="If True, no further rules will be processed after this one if any document is queued.",
|
||||||
verbose_name="Stop processing further rules",
|
verbose_name="Stop processing further rules",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -272,6 +272,24 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["success"], True)
|
self.assertEqual(response.data["success"], True)
|
||||||
|
|
||||||
|
def test_mail_account_test_existing_nonexistent_id_forbidden(self) -> None:
|
||||||
|
response = self.client.post(
|
||||||
|
f"{self.ENDPOINT}test/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": 999999,
|
||||||
|
"imap_server": "server.example.com",
|
||||||
|
"imap_port": 443,
|
||||||
|
"imap_security": MailAccount.ImapSecurity.SSL,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "******",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(response.content.decode(), "Insufficient permissions")
|
||||||
|
|
||||||
def test_get_mail_accounts_owner_aware(self) -> None:
|
def test_get_mail_accounts_owner_aware(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from datetime import timedelta
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.db import DatabaseError
|
from django.db import DatabaseError
|
||||||
@@ -1734,6 +1735,10 @@ class TestMailAccountTestView(APITestCase):
|
|||||||
username="testuser",
|
username="testuser",
|
||||||
password="testpassword",
|
password="testpassword",
|
||||||
)
|
)
|
||||||
|
self.user.user_permissions.add(
|
||||||
|
*Permission.objects.filter(codename__in=["add_mailaccount"]),
|
||||||
|
)
|
||||||
|
self.user.save()
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
self.url = "/api/mail_accounts/test/"
|
self.url = "/api/mail_accounts/test/"
|
||||||
|
|
||||||
@@ -1850,6 +1855,56 @@ class TestMailAccountTestView(APITestCase):
|
|||||||
expected_str = "Unable to refresh oauth token"
|
expected_str = "Unable to refresh oauth token"
|
||||||
self.assertIn(expected_str, error_str)
|
self.assertIn(expected_str, error_str)
|
||||||
|
|
||||||
|
def test_mail_account_test_view_existing_forbidden_for_other_owner(self) -> None:
|
||||||
|
other_user = User.objects.create_user(
|
||||||
|
username="otheruser",
|
||||||
|
password="testpassword",
|
||||||
|
)
|
||||||
|
existing_account = MailAccount.objects.create(
|
||||||
|
name="Owned account",
|
||||||
|
imap_server="imap.example.com",
|
||||||
|
imap_port=993,
|
||||||
|
imap_security=MailAccount.ImapSecurity.SSL,
|
||||||
|
username="admin",
|
||||||
|
password="secret",
|
||||||
|
owner=other_user,
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"id": existing_account.id,
|
||||||
|
"imap_server": "imap.example.com",
|
||||||
|
"imap_port": 993,
|
||||||
|
"imap_security": MailAccount.ImapSecurity.SSL,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "****",
|
||||||
|
"is_token": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data, format="json")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(response.content.decode(), "Insufficient permissions")
|
||||||
|
|
||||||
|
def test_mail_account_test_view_requires_add_permission_without_account_id(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
self.user.user_permissions.remove(
|
||||||
|
*Permission.objects.filter(codename__in=["add_mailaccount"]),
|
||||||
|
)
|
||||||
|
self.user.save()
|
||||||
|
data = {
|
||||||
|
"imap_server": "imap.example.com",
|
||||||
|
"imap_port": 993,
|
||||||
|
"imap_security": MailAccount.ImapSecurity.SSL,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "secret",
|
||||||
|
"is_token": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data, format="json")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(response.content.decode(), "Insufficient permissions")
|
||||||
|
|
||||||
|
|
||||||
class TestMailAccountProcess(APITestCase):
|
class TestMailAccountProcess(APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|||||||
@@ -86,13 +86,34 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin):
|
|||||||
request.data["name"] = datetime.datetime.now().isoformat()
|
request.data["name"] = datetime.datetime.now().isoformat()
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
existing_account = None
|
||||||
|
account_id = request.data.get("id")
|
||||||
|
|
||||||
# account exists, use the password from there instead of *** and refresh_token / expiration
|
# testing a new connection requires add permission
|
||||||
|
if account_id is None and not request.user.has_perms(
|
||||||
|
["paperless_mail.add_mailaccount"],
|
||||||
|
):
|
||||||
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
|
# testing an existing account requires change permission on that account
|
||||||
|
if account_id is not None:
|
||||||
|
try:
|
||||||
|
existing_account = MailAccount.objects.get(pk=account_id)
|
||||||
|
except (TypeError, ValueError, MailAccount.DoesNotExist):
|
||||||
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
|
if not has_perms_owner_aware(
|
||||||
|
request.user,
|
||||||
|
"change_mailaccount",
|
||||||
|
existing_account,
|
||||||
|
):
|
||||||
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
|
# account exists, use the password from there instead of ***
|
||||||
if (
|
if (
|
||||||
len(serializer.validated_data.get("password").replace("*", "")) == 0
|
len(serializer.validated_data.get("password").replace("*", "")) == 0
|
||||||
and request.data["id"] is not None
|
and existing_account is not None
|
||||||
):
|
):
|
||||||
existing_account = MailAccount.objects.get(pk=request.data["id"])
|
|
||||||
serializer.validated_data["password"] = existing_account.password
|
serializer.validated_data["password"] = existing_account.password
|
||||||
serializer.validated_data["account_type"] = existing_account.account_type
|
serializer.validated_data["account_type"] = existing_account.account_type
|
||||||
serializer.validated_data["refresh_token"] = existing_account.refresh_token
|
serializer.validated_data["refresh_token"] = existing_account.refresh_token
|
||||||
@@ -106,7 +127,8 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin):
|
|||||||
) as M:
|
) as M:
|
||||||
try:
|
try:
|
||||||
if (
|
if (
|
||||||
account.is_token
|
existing_account is not None
|
||||||
|
and account.is_token
|
||||||
and account.expiration is not None
|
and account.expiration is not None
|
||||||
and account.expiration < timezone.now()
|
and account.expiration < timezone.now()
|
||||||
):
|
):
|
||||||
@@ -248,6 +270,7 @@ class OauthCallbackView(GenericAPIView):
|
|||||||
imap_server=imap_server,
|
imap_server=imap_server,
|
||||||
refresh_token=refresh_token,
|
refresh_token=refresh_token,
|
||||||
expiration=timezone.now() + timedelta(seconds=expires_in),
|
expiration=timezone.now() + timedelta(seconds=expires_in),
|
||||||
|
owner=request.user,
|
||||||
defaults=defaults,
|
defaults=defaults,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -3019,7 +3019,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.20.7"
|
version = "2.20.8"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
|||||||
Reference in New Issue
Block a user