mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-28 05:33:24 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d706d2a9fe | ||
|
|
5caa3327fd | ||
|
|
c72c8d1574 | ||
|
|
b1f5445689 | ||
|
|
02b2f55248 | ||
|
|
a2ea3b9e76 | ||
|
|
a298568962 | ||
|
|
f6f37898e8 | ||
|
|
0cfe33720a | ||
|
|
14b17ddf8d | ||
|
|
9009fbb4e9 | ||
|
|
94651d817d | ||
|
|
1630e78aac | ||
|
|
4e79553489 | ||
|
|
f24d58a383 | ||
|
|
d486dd1205 | ||
|
|
406bc1a233 | ||
|
|
808746c28c | ||
|
|
ff6d73ce17 | ||
|
|
b4068d38bc | ||
|
|
139739c2a2 | ||
|
|
aad75b4981 |
+4
-1
@@ -59,10 +59,13 @@ The following are not generally considered vulnerabilities unless accompanied by
|
|||||||
- large uploads or resource usage that do not bypass documented limits or privileges
|
- large uploads or resource usage that do not bypass documented limits or privileges
|
||||||
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
|
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
|
||||||
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
|
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
|
||||||
- reports that rely on admin-level access, workflow-editing privileges, shell access, or other high-trust roles unless they demonstrate an unintended privilege boundary bypass
|
- pickle deserialization of internal data from trusted components such as the Redis-compatible broker or Paperless-ngx data directory
|
||||||
|
- users with permission to edit users granting themselves additional privileges; this is expected behavior for that trusted permission
|
||||||
|
- reports that rely on admin-level access, application-configuration access, workflow-editing privileges, shell access, or other high-trust roles unless they demonstrate an unintended privilege boundary bypass
|
||||||
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
|
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
|
||||||
- missing limits or hardening settings presented without concrete impact
|
- 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
|
- 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
|
||||||
- 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
|
- 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
|
## Transparency
|
||||||
|
|||||||
@@ -1,5 +1,220 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.1.0
|
||||||
|
|
||||||
|
### Features / Enhancements
|
||||||
|
|
||||||
|
- Enhancement: Apply AI suggestions workflow action [@shamoon](https://github.com/shamoon) ([#13639](https://github.com/paperless-ngx/paperless-ngx/pull/13639))
|
||||||
|
- Enhancement: welcome widget visual tweaks [@shamoon](https://github.com/shamoon) ([#13794](https://github.com/paperless-ngx/paperless-ngx/pull/13794))
|
||||||
|
- Enhancement: support using remote OCR engines selectively [@shamoon](https://github.com/shamoon) ([#13633](https://github.com/paperless-ngx/paperless-ngx/pull/13633))
|
||||||
|
- Enhancement: websocket heartbeat [@oktupol](https://github.com/oktupol) ([#13739](https://github.com/paperless-ngx/paperless-ngx/pull/13739))
|
||||||
|
- Enhancement: more v3 ui tweaks [@shamoon](https://github.com/shamoon) ([#13774](https://github.com/paperless-ngx/paperless-ngx/pull/13774))
|
||||||
|
- Tweak: better support long list of views in documents list [@shamoon](https://github.com/shamoon) ([#13769](https://github.com/paperless-ngx/paperless-ngx/pull/13769))
|
||||||
|
- QoL: add count badge to versions dropdown [@shamoon](https://github.com/shamoon) ([#13753](https://github.com/paperless-ngx/paperless-ngx/pull/13753))
|
||||||
|
- Tweakhancement: add jitter to IMAP polling schedule [@shamoon](https://github.com/shamoon) ([#13734](https://github.com/paperless-ngx/paperless-ngx/pull/13734))
|
||||||
|
- Enhancement: sync OIDC groups to superuser and staff roles [@BeSovereign](https://github.com/BeSovereign) ([#13060](https://github.com/paperless-ngx/paperless-ngx/pull/13060))
|
||||||
|
- Enhancement: merge documents as versions [@shamoon](https://github.com/shamoon) ([#13515](https://github.com/paperless-ngx/paperless-ngx/pull/13515))
|
||||||
|
- Tweak: adjust modal proportions for small screens [@shamoon](https://github.com/shamoon) ([#13728](https://github.com/paperless-ngx/paperless-ngx/pull/13728))
|
||||||
|
- Refactor: render paperless\_ai prompts via Jinja2 templates instead of f-strings [@stumpylog](https://github.com/stumpylog) ([#13698](https://github.com/paperless-ngx/paperless-ngx/pull/13698))
|
||||||
|
- Tweak: small visual tweaks / improvements \& fixes [@shamoon](https://github.com/shamoon) ([#13700](https://github.com/paperless-ngx/paperless-ngx/pull/13700))
|
||||||
|
- Enhancement: prefer existing tags, types, correspondents, and storage paths in AI suggestions [@stumpylog](https://github.com/stumpylog) ([#13676](https://github.com/paperless-ngx/paperless-ngx/pull/13676))
|
||||||
|
- Tweak: tweak permissions menu labels for shared user-dependent views [@shamoon](https://github.com/shamoon) ([#13685](https://github.com/paperless-ngx/paperless-ngx/pull/13685))
|
||||||
|
- Feature: Allow selection of compression type and and level during export [@stumpylog](https://github.com/stumpylog) ([#13661](https://github.com/paperless-ngx/paperless-ngx/pull/13661))
|
||||||
|
- Enhancement: customizable icons for saved views [@shamoon](https://github.com/shamoon) ([#13388](https://github.com/paperless-ngx/paperless-ngx/pull/13388))
|
||||||
|
- Enhancement: Add --url argument to document\_fuzzy\_match to improve output [@lukyjay](https://github.com/lukyjay) ([#13123](https://github.com/paperless-ngx/paperless-ngx/pull/13123))
|
||||||
|
- Feature: Updates remote OCR parser to respect the OCR mode setting [@stumpylog](https://github.com/stumpylog) ([#13408](https://github.com/paperless-ngx/paperless-ngx/pull/13408))
|
||||||
|
- Tweak: improve no ML suggestions UX [@shamoon](https://github.com/shamoon) ([#13621](https://github.com/paperless-ngx/paperless-ngx/pull/13621))
|
||||||
|
- Performance: reduce memory and I/O overhead of the document exporter during zip exports [@stumpylog](https://github.com/stumpylog) ([#13490](https://github.com/paperless-ngx/paperless-ngx/pull/13490))
|
||||||
|
- QoL: make name button text on attribute pages selectable [@shamoon](https://github.com/shamoon) ([#13592](https://github.com/paperless-ngx/paperless-ngx/pull/13592))
|
||||||
|
- Performance: More efficient mail fetching [@stumpylog](https://github.com/stumpylog) ([#13432](https://github.com/paperless-ngx/paperless-ngx/pull/13432))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: prevent config autocomplete craziness [@shamoon](https://github.com/shamoon) ([#13808](https://github.com/paperless-ngx/paperless-ngx/pull/13808))
|
||||||
|
- Fix/performance: prevent token reuse in dropdown filtering, also a perf thing [@shamoon](https://github.com/shamoon) ([#13804](https://github.com/paperless-ngx/paperless-ngx/pull/13804))
|
||||||
|
- Fix: exclude version documents from bulk edit "all" [@shamoon](https://github.com/shamoon) ([#13791](https://github.com/paperless-ngx/paperless-ngx/pull/13791))
|
||||||
|
- Fix: fix bottom mobile nav buttons on Android [@shamoon](https://github.com/shamoon) ([#13780](https://github.com/paperless-ngx/paperless-ngx/pull/13780))
|
||||||
|
- Fix: lazy import guardian modules to fix search language setting [@shamoon](https://github.com/shamoon) ([#13768](https://github.com/paperless-ngx/paperless-ngx/pull/13768))
|
||||||
|
- Fix: version indexing fixes [@shamoon](https://github.com/shamoon) ([#13737](https://github.com/paperless-ngx/paperless-ngx/pull/13737))
|
||||||
|
- Fix: append charset to file response for text files [@shamoon](https://github.com/shamoon) ([#13759](https://github.com/paperless-ngx/paperless-ngx/pull/13759))
|
||||||
|
- Chore: pin Apache Tika images to 3.3.1 [@shamoon](https://github.com/shamoon) ([#13758](https://github.com/paperless-ngx/paperless-ngx/pull/13758))
|
||||||
|
- Fix: align bulk edit object perms with document model [@shamoon](https://github.com/shamoon) ([#13757](https://github.com/paperless-ngx/paperless-ngx/pull/13757))
|
||||||
|
- Fix: use selected version for doc detail emailing [@shamoon](https://github.com/shamoon) ([#13738](https://github.com/paperless-ngx/paperless-ngx/pull/13738))
|
||||||
|
- Fix: hide version delete button without global perms [@shamoon](https://github.com/shamoon) ([#13735](https://github.com/paperless-ngx/paperless-ngx/pull/13735))
|
||||||
|
- Fix: dont re-render path template when checking collisions [@shamoon](https://github.com/shamoon) ([#13718](https://github.com/paperless-ngx/paperless-ngx/pull/13718))
|
||||||
|
- Fix: DocumentClassifierSchema bounds [@shamoon](https://github.com/shamoon) ([#13707](https://github.com/paperless-ngx/paperless-ngx/pull/13707))
|
||||||
|
- Fix: remove shadow around attribute pages [@shamoon](https://github.com/shamoon) ([#13696](https://github.com/paperless-ngx/paperless-ngx/pull/13696))
|
||||||
|
- Zen: correct dropdown corner radius visual defect [@shamoon](https://github.com/shamoon) ([#13695](https://github.com/paperless-ngx/paperless-ngx/pull/13695))
|
||||||
|
- Fix: handle Android keyboard popper overlay [@shamoon](https://github.com/shamoon) ([#13694](https://github.com/paperless-ngx/paperless-ngx/pull/13694))
|
||||||
|
- Fix: only show create when there is text, hide set values if no fields in cf bulk edit dropdown [@shamoon](https://github.com/shamoon) ([#13688](https://github.com/paperless-ngx/paperless-ngx/pull/13688))
|
||||||
|
- Fix: reopen a fresh Tantivy index per write to prevent orphaned segment files [@stumpylog](https://github.com/stumpylog) ([#13682](https://github.com/paperless-ngx/paperless-ngx/pull/13682))
|
||||||
|
- Fix: fix validation of workflow title assignment [@maxtruxa](https://github.com/maxtruxa) ([#13659](https://github.com/paperless-ngx/paperless-ngx/pull/13659))
|
||||||
|
- Fix: dont clip search dropdown on mobile [@shamoon](https://github.com/shamoon) ([#13675](https://github.com/paperless-ngx/paperless-ngx/pull/13675))
|
||||||
|
- Fix: include sharelink bundle perms in WebUI [@shamoon](https://github.com/shamoon) ([#13664](https://github.com/paperless-ngx/paperless-ngx/pull/13664))
|
||||||
|
- Fix: add pagination to saved views management page [@shamoon](https://github.com/shamoon) ([#13646](https://github.com/paperless-ngx/paperless-ngx/pull/13646))
|
||||||
|
- Fix: fixes for workflow assign custom field values [@shamoon](https://github.com/shamoon) ([#13630](https://github.com/paperless-ngx/paperless-ngx/pull/13630))
|
||||||
|
- Fix: deny deactivated users in permission filtering and auto-login [@stumpylog](https://github.com/stumpylog) ([#13623](https://github.com/paperless-ngx/paperless-ngx/pull/13623))
|
||||||
|
- Fix: check bulk mail delete permissions for the whole batch up front [@stumpylog](https://github.com/stumpylog) ([#13620](https://github.com/paperless-ngx/paperless-ngx/pull/13620))
|
||||||
|
- Fix: Allow DRF to validate the maximum API key length [@stumpylog](https://github.com/stumpylog) ([#13614](https://github.com/paperless-ngx/paperless-ngx/pull/13614))
|
||||||
|
- Fix: render PDF form values in annotation layer [@shamoon](https://github.com/shamoon) ([#13607](https://github.com/paperless-ngx/paperless-ngx/pull/13607))
|
||||||
|
- QoL: disable name button without perms [@shamoon](https://github.com/shamoon) ([#13606](https://github.com/paperless-ngx/paperless-ngx/pull/13606))
|
||||||
|
- Fix: prevent debounce overwrites in advanced search field, also improve Esc behavior [@shamoon](https://github.com/shamoon) ([#13602](https://github.com/paperless-ngx/paperless-ngx/pull/13602))
|
||||||
|
- Fix: raise ParseError on remote OCR failure instead of silently continuing [@stumpylog](https://github.com/stumpylog) ([#13574](https://github.com/paperless-ngx/paperless-ngx/pull/13574))
|
||||||
|
- Fix: use selected version when creating share links [@shamoon](https://github.com/shamoon) ([#13571](https://github.com/paperless-ngx/paperless-ngx/pull/13571))
|
||||||
|
- Fix: reject bulk edit permissions requests without the correct key [@shamoon](https://github.com/shamoon) ([#13563](https://github.com/paperless-ngx/paperless-ngx/pull/13563))
|
||||||
|
- Fix: correctly serve app logo specified in env [@shamoon](https://github.com/shamoon) ([#13561](https://github.com/paperless-ngx/paperless-ngx/pull/13561))
|
||||||
|
- Fix: prevent workflow passwords field type error [@shamoon](https://github.com/shamoon) ([#13552](https://github.com/paperless-ngx/paperless-ngx/pull/13552))
|
||||||
|
- Fix: correct Firefox print regression [@shamoon](https://github.com/shamoon) ([#13543](https://github.com/paperless-ngx/paperless-ngx/pull/13543))
|
||||||
|
- Fix: hide some saved view operations on management page without permissions [@shamoon](https://github.com/shamoon) ([#13542](https://github.com/paperless-ngx/paperless-ngx/pull/13542))
|
||||||
|
- Fix: disable pdfjs selection rendering [@shamoon](https://github.com/shamoon) ([#13538](https://github.com/paperless-ngx/paperless-ngx/pull/13538))
|
||||||
|
- Fix: hide sidebar drag grips with insufficient permissions [@shamoon](https://github.com/shamoon) ([#13536](https://github.com/paperless-ngx/paperless-ngx/pull/13536))
|
||||||
|
- Fix: don't re-queue a consume-folder file that is already queued and awaiting consumption [@stumpylog](https://github.com/stumpylog) ([#13526](https://github.com/paperless-ngx/paperless-ngx/pull/13526))
|
||||||
|
- Fix: correct multi-search non-adjacent queries [@shamoon](https://github.com/shamoon) ([#13504](https://github.com/paperless-ngx/paperless-ngx/pull/13504))
|
||||||
|
- Fix: Content-Disposition filename normalization [@shamoon](https://github.com/shamoon) ([#13514](https://github.com/paperless-ngx/paperless-ngx/pull/13514))
|
||||||
|
- Fix: prevent duplicated text query with multiple date queries [@shamoon](https://github.com/shamoon) ([#13522](https://github.com/paperless-ngx/paperless-ngx/pull/13522))
|
||||||
|
- Fix: crash filtering document link custom fields with an unset or unrelated field present [@ggouzi](https://github.com/ggouzi) ([#13518](https://github.com/paperless-ngx/paperless-ngx/pull/13518))
|
||||||
|
- Fix: parse unpadded yyyy-mm-dd date input regardless of locale [@Se1foo](https://github.com/Se1foo) ([#13501](https://github.com/paperless-ngx/paperless-ngx/pull/13501))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Documentation: clarify default OCR mode changes in v3 [@shamoon](https://github.com/shamoon) ([#13666](https://github.com/paperless-ngx/paperless-ngx/pull/13666))
|
||||||
|
- Fix: fixes for workflow assign custom field values [@shamoon](https://github.com/shamoon) ([#13630](https://github.com/paperless-ngx/paperless-ngx/pull/13630))
|
||||||
|
- Documentation: add wiki links for AI stuff and parser plugins [@shamoon](https://github.com/shamoon) ([#13626](https://github.com/paperless-ngx/paperless-ngx/pull/13626))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13481](https://github.com/paperless-ngx/paperless-ngx/pull/13481))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>23 changes</summary>
|
||||||
|
|
||||||
|
- Chore: Upgrade Docker image to Python 3.14 [@stumpylog](https://github.com/stumpylog) ([#13721](https://github.com/paperless-ngx/paperless-ngx/pull/13721))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13709](https://github.com/paperless-ngx/paperless-ngx/pull/13709))
|
||||||
|
- Chore: update, reorg some npm deps [@shamoon](https://github.com/shamoon) ([#13716](https://github.com/paperless-ngx/paperless-ngx/pull/13716))
|
||||||
|
- Chore: update fpdf2 to 2.8.8 [@shamoon](https://github.com/shamoon) ([#13629](https://github.com/paperless-ngx/paperless-ngx/pull/13629))
|
||||||
|
- Chore: update pnpm, add blockExoticSubdeps [@shamoon](https://github.com/shamoon) ([#13628](https://github.com/paperless-ngx/paperless-ngx/pull/13628))
|
||||||
|
- Chore(deps): Bump h2 from 4.3.0 to 4.4.1 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13593](https://github.com/paperless-ngx/paperless-ngx/pull/13593))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 6.1.200 to 6.2.108 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13594](https://github.com/paperless-ngx/paperless-ngx/pull/13594))
|
||||||
|
- Chore(deps): Bump cryptography from 48.0.1 to 50.0.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13588](https://github.com/paperless-ngx/paperless-ngx/pull/13588))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13539](https://github.com/paperless-ngx/paperless-ngx/pull/13539))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13535](https://github.com/paperless-ngx/paperless-ngx/pull/13535))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.14.1 to 3.14.3 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13537](https://github.com/paperless-ngx/paperless-ngx/pull/13537))
|
||||||
|
- Chore(deps-dev): Bump zensical from 0.0.47 to 0.0.51 in the development group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13533](https://github.com/paperless-ngx/paperless-ngx/pull/13533))
|
||||||
|
- Chore(deps-dev): Bump postcss from 8.5.22 to 8.5.25 in /src/paperless\_mail/templates in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13534](https://github.com/paperless-ngx/paperless-ngx/pull/13534))
|
||||||
|
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13532](https://github.com/paperless-ngx/paperless-ngx/pull/13532))
|
||||||
|
- Chore: ruff 0.16 upgrade [@stumpylog](https://github.com/stumpylog) ([#13531](https://github.com/paperless-ngx/paperless-ngx/pull/13531))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13481](https://github.com/paperless-ngx/paperless-ngx/pull/13481))
|
||||||
|
- docker(deps): bump astral-sh/uv from 0.11.28-python3.12-trixie-slim to 0.11.32-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#13472](https://github.com/paperless-ngx/paperless-ngx/pull/13472))
|
||||||
|
- docker-compose(deps): bump nginx from 1.31.2-alpine to 1.31.3-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#13470](https://github.com/paperless-ngx/paperless-ngx/pull/13470))
|
||||||
|
- docker-compose(deps): Bump greenmail/standalone from 2.1.9 to 2.1.11 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#13469](https://github.com/paperless-ngx/paperless-ngx/pull/13469))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 18 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13476](https://github.com/paperless-ngx/paperless-ngx/pull/13476))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.61.1 to 1.62.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13480](https://github.com/paperless-ngx/paperless-ngx/pull/13480))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13477](https://github.com/paperless-ngx/paperless-ngx/pull/13477))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 26.1.0 to 26.1.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13479](https://github.com/paperless-ngx/paperless-ngx/pull/13479))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>91 changes</summary>
|
||||||
|
|
||||||
|
- Fix: prevent config autocomplete craziness [@shamoon](https://github.com/shamoon) ([#13808](https://github.com/paperless-ngx/paperless-ngx/pull/13808))
|
||||||
|
- Fix/performance: prevent token reuse in dropdown filtering, also a perf thing [@shamoon](https://github.com/shamoon) ([#13804](https://github.com/paperless-ngx/paperless-ngx/pull/13804))
|
||||||
|
- Enhancement: Apply AI suggestions workflow action [@shamoon](https://github.com/shamoon) ([#13639](https://github.com/paperless-ngx/paperless-ngx/pull/13639))
|
||||||
|
- Fix: exclude version documents from bulk edit "all" [@shamoon](https://github.com/shamoon) ([#13791](https://github.com/paperless-ngx/paperless-ngx/pull/13791))
|
||||||
|
- Enhancement: welcome widget visual tweaks [@shamoon](https://github.com/shamoon) ([#13794](https://github.com/paperless-ngx/paperless-ngx/pull/13794))
|
||||||
|
- Performance: fetch note authors with prefetch instead of one query each [@shamoon](https://github.com/shamoon) ([#13790](https://github.com/paperless-ngx/paperless-ngx/pull/13790))
|
||||||
|
- Tweak: more misc UI tweaks [@shamoon](https://github.com/shamoon) ([#13783](https://github.com/paperless-ngx/paperless-ngx/pull/13783))
|
||||||
|
- Enhancement: support using remote OCR engines selectively [@shamoon](https://github.com/shamoon) ([#13633](https://github.com/paperless-ngx/paperless-ngx/pull/13633))
|
||||||
|
- Fix: fix bottom mobile nav buttons on Android [@shamoon](https://github.com/shamoon) ([#13780](https://github.com/paperless-ngx/paperless-ngx/pull/13780))
|
||||||
|
- Enhancement: websocket heartbeat [@oktupol](https://github.com/oktupol) ([#13739](https://github.com/paperless-ngx/paperless-ngx/pull/13739))
|
||||||
|
- Fix: lazy import guardian modules to fix search language setting [@shamoon](https://github.com/shamoon) ([#13768](https://github.com/paperless-ngx/paperless-ngx/pull/13768))
|
||||||
|
- Enhancement: more v3 ui tweaks [@shamoon](https://github.com/shamoon) ([#13774](https://github.com/paperless-ngx/paperless-ngx/pull/13774))
|
||||||
|
- Chore: add some missing UI accessibility labels [@shamoon](https://github.com/shamoon) ([#13772](https://github.com/paperless-ngx/paperless-ngx/pull/13772))
|
||||||
|
- Chore: refactor permission checkbox live changes [@shamoon](https://github.com/shamoon) ([#13771](https://github.com/paperless-ngx/paperless-ngx/pull/13771))
|
||||||
|
- Tweak: better support long list of views in documents list [@shamoon](https://github.com/shamoon) ([#13769](https://github.com/paperless-ngx/paperless-ngx/pull/13769))
|
||||||
|
- Fix: version indexing fixes [@shamoon](https://github.com/shamoon) ([#13737](https://github.com/paperless-ngx/paperless-ngx/pull/13737))
|
||||||
|
- Fix: append charset to file response for text files [@shamoon](https://github.com/shamoon) ([#13759](https://github.com/paperless-ngx/paperless-ngx/pull/13759))
|
||||||
|
- Fix: align bulk edit object perms with document model [@shamoon](https://github.com/shamoon) ([#13757](https://github.com/paperless-ngx/paperless-ngx/pull/13757))
|
||||||
|
- QoL: add count badge to versions dropdown [@shamoon](https://github.com/shamoon) ([#13753](https://github.com/paperless-ngx/paperless-ngx/pull/13753))
|
||||||
|
- Tweakhancement: add jitter to IMAP polling schedule [@shamoon](https://github.com/shamoon) ([#13734](https://github.com/paperless-ngx/paperless-ngx/pull/13734))
|
||||||
|
- Fix: use selected version for doc detail emailing [@shamoon](https://github.com/shamoon) ([#13738](https://github.com/paperless-ngx/paperless-ngx/pull/13738))
|
||||||
|
- Fix: hide version delete button without global perms [@shamoon](https://github.com/shamoon) ([#13735](https://github.com/paperless-ngx/paperless-ngx/pull/13735))
|
||||||
|
- Enhancement: sync OIDC groups to superuser and staff roles [@BeSovereign](https://github.com/BeSovereign) ([#13060](https://github.com/paperless-ngx/paperless-ngx/pull/13060))
|
||||||
|
- Enhancement: merge documents as versions [@shamoon](https://github.com/shamoon) ([#13515](https://github.com/paperless-ngx/paperless-ngx/pull/13515))
|
||||||
|
- Tweak: adjust modal proportions for small screens [@shamoon](https://github.com/shamoon) ([#13728](https://github.com/paperless-ngx/paperless-ngx/pull/13728))
|
||||||
|
- Refactor: render paperless\_ai prompts via Jinja2 templates instead of f-strings [@stumpylog](https://github.com/stumpylog) ([#13698](https://github.com/paperless-ngx/paperless-ngx/pull/13698))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13709](https://github.com/paperless-ngx/paperless-ngx/pull/13709))
|
||||||
|
- Fix: dont re-render path template when checking collisions [@shamoon](https://github.com/shamoon) ([#13718](https://github.com/paperless-ngx/paperless-ngx/pull/13718))
|
||||||
|
- Chore: update, reorg some npm deps [@shamoon](https://github.com/shamoon) ([#13716](https://github.com/paperless-ngx/paperless-ngx/pull/13716))
|
||||||
|
- Fix: DocumentClassifierSchema bounds [@shamoon](https://github.com/shamoon) ([#13707](https://github.com/paperless-ngx/paperless-ngx/pull/13707))
|
||||||
|
- Tweak: small visual tweaks / improvements \& fixes [@shamoon](https://github.com/shamoon) ([#13700](https://github.com/paperless-ngx/paperless-ngx/pull/13700))
|
||||||
|
- Fix: remove shadow around attribute pages [@shamoon](https://github.com/shamoon) ([#13696](https://github.com/paperless-ngx/paperless-ngx/pull/13696))
|
||||||
|
- Zen: correct dropdown corner radius visual defect [@shamoon](https://github.com/shamoon) ([#13695](https://github.com/paperless-ngx/paperless-ngx/pull/13695))
|
||||||
|
- Fix: handle Android keyboard popper overlay [@shamoon](https://github.com/shamoon) ([#13694](https://github.com/paperless-ngx/paperless-ngx/pull/13694))
|
||||||
|
- Enhancement: prefer existing tags, types, correspondents, and storage paths in AI suggestions [@stumpylog](https://github.com/stumpylog) ([#13676](https://github.com/paperless-ngx/paperless-ngx/pull/13676))
|
||||||
|
- Fix: only show create when there is text, hide set values if no fields in cf bulk edit dropdown [@shamoon](https://github.com/shamoon) ([#13688](https://github.com/paperless-ngx/paperless-ngx/pull/13688))
|
||||||
|
- Fix: reopen a fresh Tantivy index per write to prevent orphaned segment files [@stumpylog](https://github.com/stumpylog) ([#13682](https://github.com/paperless-ngx/paperless-ngx/pull/13682))
|
||||||
|
- Tweak: tweak permissions menu labels for shared user-dependent views [@shamoon](https://github.com/shamoon) ([#13685](https://github.com/paperless-ngx/paperless-ngx/pull/13685))
|
||||||
|
- Fix: fix validation of workflow title assignment [@maxtruxa](https://github.com/maxtruxa) ([#13659](https://github.com/paperless-ngx/paperless-ngx/pull/13659))
|
||||||
|
- Feature: Allow selection of compression type and and level during export [@stumpylog](https://github.com/stumpylog) ([#13661](https://github.com/paperless-ngx/paperless-ngx/pull/13661))
|
||||||
|
- Fix: dont clip search dropdown on mobile [@shamoon](https://github.com/shamoon) ([#13675](https://github.com/paperless-ngx/paperless-ngx/pull/13675))
|
||||||
|
- Fix: include sharelink bundle perms in WebUI [@shamoon](https://github.com/shamoon) ([#13664](https://github.com/paperless-ngx/paperless-ngx/pull/13664))
|
||||||
|
- Enhancement: customizable icons for saved views [@shamoon](https://github.com/shamoon) ([#13388](https://github.com/paperless-ngx/paperless-ngx/pull/13388))
|
||||||
|
- Enhancement: Add --url argument to document\_fuzzy\_match to improve output [@lukyjay](https://github.com/lukyjay) ([#13123](https://github.com/paperless-ngx/paperless-ngx/pull/13123))
|
||||||
|
- Feature: Updates remote OCR parser to respect the OCR mode setting [@stumpylog](https://github.com/stumpylog) ([#13408](https://github.com/paperless-ngx/paperless-ngx/pull/13408))
|
||||||
|
- Performance: pass document chat queries as a QuerySet instead of a materialized.list [@stumpylog](https://github.com/stumpylog) ([#13638](https://github.com/paperless-ngx/paperless-ngx/pull/13638))
|
||||||
|
- Fix: add pagination to saved views management page [@shamoon](https://github.com/shamoon) ([#13646](https://github.com/paperless-ngx/paperless-ngx/pull/13646))
|
||||||
|
- Fix: fixes for workflow assign custom field values [@shamoon](https://github.com/shamoon) ([#13630](https://github.com/paperless-ngx/paperless-ngx/pull/13630))
|
||||||
|
- Fix: deny deactivated users in permission filtering and auto-login [@stumpylog](https://github.com/stumpylog) ([#13623](https://github.com/paperless-ngx/paperless-ngx/pull/13623))
|
||||||
|
- Chore: update fpdf2 to 2.8.8 [@shamoon](https://github.com/shamoon) ([#13629](https://github.com/paperless-ngx/paperless-ngx/pull/13629))
|
||||||
|
- Chore: update pnpm, add blockExoticSubdeps [@shamoon](https://github.com/shamoon) ([#13628](https://github.com/paperless-ngx/paperless-ngx/pull/13628))
|
||||||
|
- Fix: check bulk mail delete permissions for the whole batch up front [@stumpylog](https://github.com/stumpylog) ([#13620](https://github.com/paperless-ngx/paperless-ngx/pull/13620))
|
||||||
|
- Tweak: improve no ML suggestions UX [@shamoon](https://github.com/shamoon) ([#13621](https://github.com/paperless-ngx/paperless-ngx/pull/13621))
|
||||||
|
- Fix: Allow DRF to validate the maximum API key length [@stumpylog](https://github.com/stumpylog) ([#13614](https://github.com/paperless-ngx/paperless-ngx/pull/13614))
|
||||||
|
- Performance: unify permission-filtering backends, fixes Correspondent/Tag list slowness [@stumpylog](https://github.com/stumpylog) ([#13601](https://github.com/paperless-ngx/paperless-ngx/pull/13601))
|
||||||
|
- Performance: generalize permitted\_document\_ids into permitted\_object\_ids for any model [@stumpylog](https://github.com/stumpylog) ([#13578](https://github.com/paperless-ngx/paperless-ngx/pull/13578))
|
||||||
|
- Fix: render PDF form values in annotation layer [@shamoon](https://github.com/shamoon) ([#13607](https://github.com/paperless-ngx/paperless-ngx/pull/13607))
|
||||||
|
- QoL: disable name button without perms [@shamoon](https://github.com/shamoon) ([#13606](https://github.com/paperless-ngx/paperless-ngx/pull/13606))
|
||||||
|
- Fix: prevent debounce overwrites in advanced search field, also improve Esc behavior [@shamoon](https://github.com/shamoon) ([#13602](https://github.com/paperless-ngx/paperless-ngx/pull/13602))
|
||||||
|
- Performance: reduce memory and I/O overhead of the document exporter during zip exports [@stumpylog](https://github.com/stumpylog) ([#13490](https://github.com/paperless-ngx/paperless-ngx/pull/13490))
|
||||||
|
- Chore(deps): Bump h2 from 4.3.0 to 4.4.1 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13593](https://github.com/paperless-ngx/paperless-ngx/pull/13593))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 6.1.200 to 6.2.108 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13594](https://github.com/paperless-ngx/paperless-ngx/pull/13594))
|
||||||
|
- QoL: make name button text on attribute pages selectable [@shamoon](https://github.com/shamoon) ([#13592](https://github.com/paperless-ngx/paperless-ngx/pull/13592))
|
||||||
|
- Fix: raise ParseError on remote OCR failure instead of silently continuing [@stumpylog](https://github.com/stumpylog) ([#13574](https://github.com/paperless-ngx/paperless-ngx/pull/13574))
|
||||||
|
- Fix: use selected version when creating share links [@shamoon](https://github.com/shamoon) ([#13571](https://github.com/paperless-ngx/paperless-ngx/pull/13571))
|
||||||
|
- Chore: specify AI chat refine template [@shamoon](https://github.com/shamoon) ([#13564](https://github.com/paperless-ngx/paperless-ngx/pull/13564))
|
||||||
|
- Fix: reject bulk edit permissions requests without the correct key [@shamoon](https://github.com/shamoon) ([#13563](https://github.com/paperless-ngx/paperless-ngx/pull/13563))
|
||||||
|
- Fix: correctly serve app logo specified in env [@shamoon](https://github.com/shamoon) ([#13561](https://github.com/paperless-ngx/paperless-ngx/pull/13561))
|
||||||
|
- Fix: prevent workflow passwords field type error [@shamoon](https://github.com/shamoon) ([#13552](https://github.com/paperless-ngx/paperless-ngx/pull/13552))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13539](https://github.com/paperless-ngx/paperless-ngx/pull/13539))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13535](https://github.com/paperless-ngx/paperless-ngx/pull/13535))
|
||||||
|
- Performance: More efficient mail fetching [@stumpylog](https://github.com/stumpylog) ([#13432](https://github.com/paperless-ngx/paperless-ngx/pull/13432))
|
||||||
|
- Performance: eliminate per-document guardian permission-check causing high CPU on document lists [@stumpylog](https://github.com/stumpylog) ([#13505](https://github.com/paperless-ngx/paperless-ngx/pull/13505))
|
||||||
|
- Fix: correct Firefox print regression [@shamoon](https://github.com/shamoon) ([#13543](https://github.com/paperless-ngx/paperless-ngx/pull/13543))
|
||||||
|
- Fix: hide some saved view operations on management page without permissions [@shamoon](https://github.com/shamoon) ([#13542](https://github.com/paperless-ngx/paperless-ngx/pull/13542))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.14.1 to 3.14.3 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13537](https://github.com/paperless-ngx/paperless-ngx/pull/13537))
|
||||||
|
- Chore(deps-dev): Bump zensical from 0.0.47 to 0.0.51 in the development group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13533](https://github.com/paperless-ngx/paperless-ngx/pull/13533))
|
||||||
|
- Fix: disable pdfjs selection rendering [@shamoon](https://github.com/shamoon) ([#13538](https://github.com/paperless-ngx/paperless-ngx/pull/13538))
|
||||||
|
- Fix: hide sidebar drag grips with insufficient permissions [@shamoon](https://github.com/shamoon) ([#13536](https://github.com/paperless-ngx/paperless-ngx/pull/13536))
|
||||||
|
- Chore(deps-dev): Bump postcss from 8.5.22 to 8.5.25 in /src/paperless\_mail/templates in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13534](https://github.com/paperless-ngx/paperless-ngx/pull/13534))
|
||||||
|
- Chore: ruff 0.16 upgrade [@stumpylog](https://github.com/stumpylog) ([#13531](https://github.com/paperless-ngx/paperless-ngx/pull/13531))
|
||||||
|
- Fix: don't re-queue a consume-folder file that is already queued and awaiting consumption [@stumpylog](https://github.com/stumpylog) ([#13526](https://github.com/paperless-ngx/paperless-ngx/pull/13526))
|
||||||
|
- Fix: correct multi-search non-adjacent queries [@shamoon](https://github.com/shamoon) ([#13504](https://github.com/paperless-ngx/paperless-ngx/pull/13504))
|
||||||
|
- Fix: Content-Disposition filename normalization [@shamoon](https://github.com/shamoon) ([#13514](https://github.com/paperless-ngx/paperless-ngx/pull/13514))
|
||||||
|
- Fix: prevent duplicated text query with multiple date queries [@shamoon](https://github.com/shamoon) ([#13522](https://github.com/paperless-ngx/paperless-ngx/pull/13522))
|
||||||
|
- Fix: crash filtering document link custom fields with an unset or unrelated field present [@ggouzi](https://github.com/ggouzi) ([#13518](https://github.com/paperless-ngx/paperless-ngx/pull/13518))
|
||||||
|
- Fix: parse unpadded yyyy-mm-dd date input regardless of locale [@Se1foo](https://github.com/Se1foo) ([#13501](https://github.com/paperless-ngx/paperless-ngx/pull/13501))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 18 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13476](https://github.com/paperless-ngx/paperless-ngx/pull/13476))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.61.1 to 1.62.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13480](https://github.com/paperless-ngx/paperless-ngx/pull/13480))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13477](https://github.com/paperless-ngx/paperless-ngx/pull/13477))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 26.1.0 to 26.1.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13479](https://github.com/paperless-ngx/paperless-ngx/pull/13479))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 3.0.5
|
## paperless-ngx 3.0.5
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ common [OCR](#ocr) related settings and some frontend settings. If set, these wi
|
|||||||
preference over the settings via environment variables. If not set, the environment setting
|
preference over the settings via environment variables. If not set, the environment setting
|
||||||
or applicable default will be utilized instead.
|
or applicable default will be utilized instead.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Changing configuration from the UI requires the `AppConfig` permission, which applies
|
||||||
|
instance-wide and should be treated as an admin-level permission. See
|
||||||
|
[global permissions](usage.md#global-permissions).
|
||||||
|
|
||||||
- If you run paperless on docker, `paperless.conf` is not used.
|
- If you run paperless on docker, `paperless.conf` is not used.
|
||||||
Rather, configure paperless by copying necessary options to
|
Rather, configure paperless by copying necessary options to
|
||||||
`docker-compose.env`.
|
`docker-compose.env`.
|
||||||
@@ -1125,6 +1131,10 @@ they use underscores instead of dashes.
|
|||||||
so specifying invalid options may prevent paperless from consuming
|
so specifying invalid options may prevent paperless from consuming
|
||||||
any documents. Use with caution!
|
any documents. Use with caution!
|
||||||
|
|
||||||
|
These arguments are passed directly to OCRmyPDF, so this setting should only
|
||||||
|
be changed by trusted users. This applies to the `AppConfig` permission as well,
|
||||||
|
which allows setting these arguments from the UI.
|
||||||
|
|
||||||
Specify arguments as a JSON dictionary. Keep note of lower case
|
Specify arguments as a JSON dictionary. Keep note of lower case
|
||||||
booleans and double quoted parameter names and strings. Examples:
|
booleans and double quoted parameter names and strings. Examples:
|
||||||
|
|
||||||
|
|||||||
+21
-21
@@ -427,27 +427,27 @@ Global permissions define what areas of the app and API endpoints users can acce
|
|||||||
determine if a user can create, edit, delete or view _any_ documents, but individual documents themselves
|
determine if a user can create, edit, delete or view _any_ documents, but individual documents themselves
|
||||||
still have "object-level" permissions.
|
still have "object-level" permissions.
|
||||||
|
|
||||||
| Type | Details |
|
| Type | Details |
|
||||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| AppConfig | _Change_ or higher permissions grants access to the "Application Configuration" area. |
|
| AppConfig | _Change_ or higher permissions grants access to the "Application Configuration" area.<br/>:warning: **This is a trusted, admin-level permission.** Application configuration applies instance-wide and some settings, such as OCR arguments, are passed directly to the underlying processing tools. |
|
||||||
| Correspondent | Add, edit, delete or view Correspondents. |
|
| Correspondent | Add, edit, delete or view Correspondents. |
|
||||||
| CustomField | Add, edit, delete or view Custom Fields. |
|
| CustomField | Add, edit, delete or view Custom Fields. |
|
||||||
| Document | Add, edit, delete or view Documents. |
|
| Document | Add, edit, delete or view Documents. |
|
||||||
| DocumentType | Add, edit, delete or view Document Types. |
|
| DocumentType | Add, edit, delete or view Document Types. |
|
||||||
| Group | Add, edit, delete or view Groups. |
|
| Group | Add, edit, delete or view Groups. |
|
||||||
| GlobalStatistics | View aggregate object counts and statistics. This does not grant access to view individual documents. |
|
| GlobalStatistics | View aggregate object counts and statistics. This does not grant access to view individual documents. |
|
||||||
| MailAccount | Add, edit, delete or view Mail Accounts. |
|
| MailAccount | Add, edit, delete or view Mail Accounts. |
|
||||||
| MailRule | Add, edit, delete or view Mail Rules. |
|
| MailRule | Add, edit, delete or view Mail Rules. |
|
||||||
| Note | Add, edit, delete or view Notes. |
|
| Note | Add, edit, delete or view Notes. |
|
||||||
| PaperlessTask | View or dismiss (_Change_) File Tasks. |
|
| PaperlessTask | View or dismiss (_Change_) File Tasks. |
|
||||||
| SavedView | Add, edit, delete or view Saved Views. |
|
| SavedView | Add, edit, delete or view Saved Views. |
|
||||||
| ShareLink | Add, delete or view Share Links. |
|
| ShareLink | Add, delete or view Share Links. |
|
||||||
| StoragePath | Add, edit, delete or view Storage Paths. |
|
| StoragePath | Add, edit, delete or view Storage Paths. |
|
||||||
| SystemMonitoring | View the system status dialog, tasks summary and their API endpoints. Admin users also retain system status access. |
|
| SystemMonitoring | View the system status dialog, tasks summary and their API endpoints. Admin users also retain system status access. |
|
||||||
| Tag | Add, edit, delete or view Tags. |
|
| Tag | Add, edit, delete or view Tags. |
|
||||||
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
|
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
|
||||||
| User | Add, edit, delete or view other user accounts via Settings > Users & Groups and `/api/users/`. These permissions are not needed for users to edit their own profile via "My Profile" or `/api/profile/`. |
|
| User | Add, edit, delete or view other user accounts via Settings > Users & Groups and `/api/users/`. These permissions are not needed for users to edit their own profile via "My Profile" or `/api/profile/`. |
|
||||||
| Workflow | Add, edit, delete or view Workflows.<br/>Note that Workflows are global; all users who can access workflows see the same set. Workflows have other permission implications — see [Workflow permissions](#workflow-permissions). |
|
| Workflow | Add, edit, delete or view Workflows.<br/>Note that Workflows are global; all users who can access workflows see the same set. Workflows have other permission implications — see [Workflow permissions](#workflow-permissions). |
|
||||||
|
|
||||||
#### Detailed Explanation of Object Permissions {#object-permissions}
|
#### Detailed Explanation of Object Permissions {#object-permissions}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "3.0.5"
|
version = "3.1.0"
|
||||||
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.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+63
-56
@@ -296,11 +296,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">89</context>
|
<context context-type="linenumber">91</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">93</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
||||||
@@ -323,11 +323,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">98</context>
|
<context context-type="linenumber">100</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">100</context>
|
<context context-type="linenumber">102</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
|
||||||
@@ -374,16 +374,16 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">56</context>
|
<context context-type="linenumber">58</context>
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
|
||||||
<context context-type="linenumber">280</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">282</context>
|
<context context-type="linenumber">282</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
|
<context context-type="linenumber">284</context>
|
||||||
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5890330709052835856" datatype="html">
|
<trans-unit id="5890330709052835856" datatype="html">
|
||||||
<source>The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.</source>
|
<source>The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.</source>
|
||||||
@@ -727,11 +727,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">316</context>
|
<context context-type="linenumber">318</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">318</context>
|
<context context-type="linenumber">320</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2272120016352772836" datatype="html">
|
<trans-unit id="2272120016352772836" datatype="html">
|
||||||
@@ -1142,11 +1142,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">240</context>
|
<context context-type="linenumber">242</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">242</context>
|
<context context-type="linenumber">244</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
@@ -1723,7 +1723,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">211</context>
|
<context context-type="linenumber">213</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
@@ -1836,11 +1836,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">303</context>
|
<context context-type="linenumber">305</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">305</context>
|
<context context-type="linenumber">307</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8492095365580052820" datatype="html">
|
<trans-unit id="8492095365580052820" datatype="html">
|
||||||
@@ -2617,11 +2617,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">264</context>
|
<context context-type="linenumber">266</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">266</context>
|
<context context-type="linenumber">268</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3818027200170621545" datatype="html">
|
<trans-unit id="3818027200170621545" datatype="html">
|
||||||
@@ -2978,11 +2978,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">294</context>
|
<context context-type="linenumber">296</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">296</context>
|
<context context-type="linenumber">298</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4569276013106377105" datatype="html">
|
<trans-unit id="4569276013106377105" datatype="html">
|
||||||
@@ -3291,104 +3291,104 @@
|
|||||||
<source>by Paperless-ngx</source>
|
<source>by Paperless-ngx</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">23,24</context>
|
<context context-type="linenumber">24,25</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7228136119811576789" datatype="html">
|
<trans-unit id="7228136119811576789" datatype="html">
|
||||||
<source>User menu</source>
|
<source>User menu</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">42</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2448391510242468907" datatype="html">
|
<trans-unit id="2448391510242468907" datatype="html">
|
||||||
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{this.settingsService.displayName}}"/></source>
|
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{this.settingsService.displayName}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">48,49</context>
|
<context context-type="linenumber">50,51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2127032578120864096" datatype="html">
|
<trans-unit id="2127032578120864096" datatype="html">
|
||||||
<source>My Profile</source>
|
<source>My Profile</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">54</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3797778920049399855" datatype="html">
|
<trans-unit id="3797778920049399855" datatype="html">
|
||||||
<source>Logout</source>
|
<source>Logout</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">59</context>
|
<context context-type="linenumber">61</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4895326106573044490" datatype="html">
|
<trans-unit id="4895326106573044490" datatype="html">
|
||||||
<source>Documentation</source>
|
<source>Documentation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">64</context>
|
<context context-type="linenumber">66</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">324,325</context>
|
<context context-type="linenumber">326,327</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">327</context>
|
<context context-type="linenumber">329</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="472206565520537964" datatype="html">
|
<trans-unit id="472206565520537964" datatype="html">
|
||||||
<source>Saved views</source>
|
<source>Saved views</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">108,109</context>
|
<context context-type="linenumber">110,111</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">138,139</context>
|
<context context-type="linenumber">140,141</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6988090220128974198" datatype="html">
|
<trans-unit id="6988090220128974198" datatype="html">
|
||||||
<source>Open documents</source>
|
<source>Open documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">147,148</context>
|
<context context-type="linenumber">149,150</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5687256342387781369" datatype="html">
|
<trans-unit id="5687256342387781369" datatype="html">
|
||||||
<source>Close all</source>
|
<source>Close all</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">169</context>
|
<context context-type="linenumber">171</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">171</context>
|
<context context-type="linenumber">173</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3897348120591552265" datatype="html">
|
<trans-unit id="3897348120591552265" datatype="html">
|
||||||
<source>Manage</source>
|
<source>Manage</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">180,181</context>
|
<context context-type="linenumber">182,183</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8008131619909556709" datatype="html">
|
<trans-unit id="8008131619909556709" datatype="html">
|
||||||
<source>Attributes</source>
|
<source>Attributes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">188</context>
|
<context context-type="linenumber">190</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">190</context>
|
<context context-type="linenumber">192</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7437910965833684826" datatype="html">
|
<trans-unit id="7437910965833684826" datatype="html">
|
||||||
<source>Correspondents</source>
|
<source>Correspondents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">216</context>
|
<context context-type="linenumber">218</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
|
||||||
@@ -3407,7 +3407,7 @@
|
|||||||
<source>Document types</source>
|
<source>Document types</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">221</context>
|
<context context-type="linenumber">223</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.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
|
||||||
@@ -3418,7 +3418,7 @@
|
|||||||
<source>Storage paths</source>
|
<source>Storage paths</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">226</context>
|
<context context-type="linenumber">228</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.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
|
||||||
@@ -3429,7 +3429,7 @@
|
|||||||
<source>Custom fields</source>
|
<source>Custom fields</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">231</context>
|
<context context-type="linenumber">233</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -3448,11 +3448,11 @@
|
|||||||
<source>Workflows</source>
|
<source>Workflows</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">249</context>
|
<context context-type="linenumber">251</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">251</context>
|
<context context-type="linenumber">253</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
||||||
@@ -3463,78 +3463,78 @@
|
|||||||
<source>Mail</source>
|
<source>Mail</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">257</context>
|
<context context-type="linenumber">259</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">259</context>
|
<context context-type="linenumber">261</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html">
|
<trans-unit id="7844706011418789951" datatype="html">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">274,275</context>
|
<context context-type="linenumber">276,277</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3008420115644088420" datatype="html">
|
<trans-unit id="3008420115644088420" datatype="html">
|
||||||
<source>Configuration</source>
|
<source>Configuration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">287</context>
|
<context context-type="linenumber">289</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.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">289</context>
|
<context context-type="linenumber">291</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1534029177398918729" datatype="html">
|
<trans-unit id="1534029177398918729" datatype="html">
|
||||||
<source>GitHub</source>
|
<source>GitHub</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">334</context>
|
<context context-type="linenumber">336</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4112664765954374539" datatype="html">
|
<trans-unit id="4112664765954374539" datatype="html">
|
||||||
<source>is available.</source>
|
<source>is available.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">344</context>
|
<context context-type="linenumber">346</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1175891574282637937" datatype="html">
|
<trans-unit id="1175891574282637937" datatype="html">
|
||||||
<source>Click to view.</source>
|
<source>Click to view.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">344</context>
|
<context context-type="linenumber">346</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9811291095862612" datatype="html">
|
<trans-unit id="9811291095862612" datatype="html">
|
||||||
<source>Paperless-ngx can automatically check for updates</source>
|
<source>Paperless-ngx can automatically check for updates</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">348</context>
|
<context context-type="linenumber">350</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="894819944961861800" datatype="html">
|
<trans-unit id="894819944961861800" datatype="html">
|
||||||
<source> How does this work? </source>
|
<source> How does this work? </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">356</context>
|
<context context-type="linenumber">358</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="509090351011426949" datatype="html">
|
<trans-unit id="509090351011426949" datatype="html">
|
||||||
<source>Update available</source>
|
<source>Update available</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">368</context>
|
<context context-type="linenumber">370</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1329827712962827905" datatype="html">
|
<trans-unit id="1329827712962827905" datatype="html">
|
||||||
<source>Configure update checking</source>
|
<source>Configure update checking</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">373</context>
|
<context context-type="linenumber">375</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1542489069631984294" datatype="html">
|
<trans-unit id="1542489069631984294" datatype="html">
|
||||||
@@ -4639,7 +4639,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||||
<context context-type="linenumber">53</context>
|
<context context-type="linenumber">58</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5324147361912094446" datatype="html">
|
<trans-unit id="5324147361912094446" datatype="html">
|
||||||
@@ -6488,7 +6488,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.html</context>
|
||||||
@@ -6614,6 +6614,13 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Used for both types, correspondents, storage paths</note>
|
<note priority="1" from="description">Used for both types, correspondents, storage paths</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6945988051184690124" datatype="html">
|
||||||
|
<source>Remove item</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||||
|
<context context-type="linenumber">41</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="3686284950598311784" datatype="html">
|
<trans-unit id="3686284950598311784" datatype="html">
|
||||||
<source>Private</source>
|
<source>Private</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "3.0.5",
|
"version": "3.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
infoLink="configuration">
|
infoLink="configuration">
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">
|
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4" autocomplete="off">
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||||
@for (category of optionCategories; track category) {
|
@for (category of optionCategories; track category) {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||||
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||||
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
||||||
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
|
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]" autocomplete="new-password"></pngx-input-password> }
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (option.note) {
|
@if (option.note) {
|
||||||
|
|||||||
@@ -4,27 +4,29 @@
|
|||||||
(click)="closeMobileSearch(); toggleMenuCollapsed()">
|
(click)="closeMobileSearch(); toggleMenuCollapsed()">
|
||||||
<i-bs width="1.5em" height="1.5em" name="list"></i-bs>
|
<i-bs width="1.5em" height="1.5em" name="list"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand d-flex align-items-center me-0 ps-md-3 py-0 order-sm-0"
|
<div class="navbar-brand-container order-sm-0">
|
||||||
|
<a class="navbar-brand d-flex align-items-center me-0 ps-md-3 py-0"
|
||||||
[ngClass]="{ 'slim': slimSidebarEnabled, '' : !slimSidebarEnabled }"
|
[ngClass]="{ 'slim': slimSidebarEnabled, '' : !slimSidebarEnabled }"
|
||||||
routerLink="/dashboard"
|
routerLink="/dashboard"
|
||||||
tourAnchor="tour.intro">
|
tourAnchor="tour.intro">
|
||||||
@if (!hasCustomBranding) {
|
@if (!hasCustomBranding) {
|
||||||
<pngx-logo extra_classes="navbar-official-logo px-1" height="2.4rem"></pngx-logo>
|
<pngx-logo extra_classes="navbar-official-logo px-1" height="2.4rem"></pngx-logo>
|
||||||
<pngx-brand-mark class="brand-mark brand-mark-slim d-none"></pngx-brand-mark>
|
<pngx-brand-mark class="brand-mark brand-mark-slim d-none"></pngx-brand-mark>
|
||||||
} @else {
|
|
||||||
@if (customAppLogo) {
|
|
||||||
<img class="brand-logo" [src]="customAppLogo" alt="" />
|
|
||||||
} @else {
|
} @else {
|
||||||
<pngx-brand-mark class="brand-mark"></pngx-brand-mark>
|
@if (customAppLogo) {
|
||||||
}
|
<img class="brand-logo" [src]="customAppLogo" alt="" />
|
||||||
<div class="brand-copy ms-2 text-truncate" [class.d-md-none]="slimSidebarEnabled">
|
} @else {
|
||||||
<span class="brand-title text-truncate">{{ appTitle }}</span>
|
<pngx-brand-mark class="brand-mark"></pngx-brand-mark>
|
||||||
@if (customAppTitle) {
|
|
||||||
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
<div class="brand-copy ms-2 text-truncate" [class.d-md-none]="slimSidebarEnabled">
|
||||||
}
|
<span class="brand-title text-truncate">{{ appTitle }}</span>
|
||||||
</a>
|
@if (customAppTitle) {
|
||||||
|
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="search-container flex-grow-1 py-2 pb-3 pb-sm-2 me-sm-auto order-3 order-sm-1"
|
<div class="search-container flex-grow-1 py-2 pb-3 pb-sm-2 me-sm-auto order-3 order-sm-1"
|
||||||
[class.mobile-hidden]="mobileSearchHidden()">
|
[class.mobile-hidden]="mobileSearchHidden()">
|
||||||
<div class="col-12 header-search mx-auto">
|
<div class="col-12 header-search mx-auto">
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
||||||
<li ngbDropdown class="nav-item dropdown">
|
<li ngbDropdown class="nav-item dropdown">
|
||||||
<button class="btn navbar-action border-0" id="userDropdown" ngbDropdownToggle aria-label="User menu" i18n-aria-label>
|
<button class="btn navbar-action border-0 me-1 me-md-2" id="userDropdown" ngbDropdownToggle aria-label="User menu" i18n-aria-label>
|
||||||
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
||||||
<span class="small ms-2 d-none d-sm-inline">
|
<span class="small ms-2 d-none d-sm-inline">
|
||||||
{{this.settingsService.displayName}}
|
{{this.settingsService.displayName}}
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .navbar-official-logo {
|
::ng-deep .navbar-official-logo {
|
||||||
|
will-change: filter; // Safari repaints the whole navbar on filter change without this
|
||||||
filter: drop-shadow(0 1px 2px rgba(var(--pngx-navbar-brand-shadow-rgb), .3));
|
filter: drop-shadow(0 1px 2px rgba(var(--pngx-navbar-brand-shadow-rgb), .3));
|
||||||
transition: filter .15s ease-in-out;
|
transition: filter .15s ease-in-out;
|
||||||
|
|
||||||
@@ -436,6 +437,19 @@ main {
|
|||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// true center the search with equal flex widths
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.navbar-brand-container,
|
||||||
|
.navbar > ul {
|
||||||
|
display: flex; // so the brand link stays as wide as its contents
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > ul {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:host ::ng-deep .navbar-action {
|
:host ::ng-deep .navbar-action {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -476,6 +490,7 @@ main {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-brand-container,
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
grid-area: brand;
|
grid-area: brand;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ describe('PasswordComponent', () => {
|
|||||||
// expect(component.value).toEqual('foo')
|
// expect(component.value).toEqual('foo')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not offer itself to browser autofill by default', () => {
|
||||||
|
expect(input.getAttribute('autocomplete')).toEqual('off')
|
||||||
|
})
|
||||||
|
|
||||||
it('should support toggling field visibility', () => {
|
it('should support toggling field visibility', () => {
|
||||||
expect(input.type).toEqual('password')
|
expect(input.type).toEqual('password')
|
||||||
component.showReveal = true
|
component.showReveal = true
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class PasswordComponent extends AbstractInputComponent<string> {
|
|||||||
showReveal: boolean = false
|
showReveal: boolean = false
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
autocomplete: string
|
autocomplete: string = 'off'
|
||||||
|
|
||||||
public textVisible: boolean = false
|
public textVisible: boolean = false
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,16 @@
|
|||||||
(focus)="clearLastSearchTerm()"
|
(focus)="clearLastSearchTerm()"
|
||||||
(clear)="clearLastSearchTerm()"
|
(clear)="clearLastSearchTerm()"
|
||||||
(blur)="onBlur()">
|
(blur)="onBlur()">
|
||||||
<ng-template ng-label-tmp let-item="item">
|
<ng-template ng-label-tmp let-item="item" let-clear="clear">
|
||||||
@if (iconField && item[iconField]) {
|
@if (multiple && !disabled) {
|
||||||
<i-bs class="me-2" [name]="item[iconField]"></i-bs>
|
<span class="ng-value-icon left" role="button" tabindex="0" (click)="clear(item)" (keydown.enter)="clear(item)" aria-label="Remove item" i18n-aria-label>×</span>
|
||||||
}
|
}
|
||||||
<span [title]="item[bindLabel]">{{item[bindLabel]}}</span>
|
<span class="ng-value-label" [title]="item[bindLabel]">
|
||||||
|
@if (iconField && item[iconField]) {
|
||||||
|
<i-bs class="me-2" [name]="item[iconField]"></i-bs>
|
||||||
|
}
|
||||||
|
{{item[bindLabel]}}
|
||||||
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template ng-option-tmp let-item="item">
|
<ng-template ng-option-tmp let-item="item">
|
||||||
@if (iconField && item[iconField]) {
|
@if (iconField && item[iconField]) {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ describe('TextComponent', () => {
|
|||||||
input = component.inputField.nativeElement
|
input = component.inputField.nativeElement
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not offer itself to browser autofill by default', () => {
|
||||||
|
expect(
|
||||||
|
component.inputField.nativeElement.getAttribute('autocomplete')
|
||||||
|
).toEqual('off')
|
||||||
|
})
|
||||||
|
|
||||||
it('should support use of input field', () => {
|
it('should support use of input field', () => {
|
||||||
expect(component.value).toBeUndefined()
|
expect(component.value).toBeUndefined()
|
||||||
input.value = 'foo'
|
input.value = 'foo'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { AbstractInputComponent } from '../abstract-input'
|
|||||||
})
|
})
|
||||||
export class TextComponent extends AbstractInputComponent<string> {
|
export class TextComponent extends AbstractInputComponent<string> {
|
||||||
@Input()
|
@Input()
|
||||||
autocomplete: string
|
autocomplete: string = 'off'
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
placeholder: string = ''
|
placeholder: string = ''
|
||||||
|
|||||||
@@ -15,6 +15,35 @@ describe('text search utilities', () => {
|
|||||||
expect(matchesSearchText('taxes 2026', 'tax receipt')).toBeFalsy()
|
expect(matchesSearchText('taxes 2026', 'tax receipt')).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not let two terms match the same word', () => {
|
||||||
|
expect(matchesSearchText('Another Tag', 'another tag th')).toBeFalsy()
|
||||||
|
expect(matchesSearchText('Another Tag', 'another tag ag')).toBeFalsy()
|
||||||
|
expect(matchesSearchText('Another Tag', 'another tag e')).toBeFalsy()
|
||||||
|
expect(matchesSearchText('Another Tag', 'another tag')).toBeTruthy()
|
||||||
|
expect(matchesSearchText('Another Tag', 'tag another')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches a single term anywhere in the value', () => {
|
||||||
|
expect(matchesSearchText('Another Tag', 'anoth')).toBeTruthy()
|
||||||
|
expect(matchesSearchText('Another Tag', 'th')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats punctuation as a separator on both sides', () => {
|
||||||
|
expect(matchesSearchText('medical-history', 'medical history')).toBeTruthy()
|
||||||
|
expect(matchesSearchText('medical history', 'medical-history')).toBeTruthy()
|
||||||
|
expect(matchesSearchText('medical-history', 'medical dental')).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches longer terms first so they cannot be starved', () => {
|
||||||
|
expect(matchesSearchText('tagger tag', 'tag tagger')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles a query with no usable terms', () => {
|
||||||
|
expect(matchesSearchText('Another Tag', '')).toBeTruthy()
|
||||||
|
// Still filters, so the dropdown can offer to create a tag named "---"
|
||||||
|
expect(matchesSearchText('Another Tag', '---')).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('matches a large set of tag names without blocking input', () => {
|
it('matches a large set of tag names without blocking input', () => {
|
||||||
const tagNames = Array.from(
|
const tagNames = Array.from(
|
||||||
{ length: 1280 },
|
{ length: 1280 },
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ import { diacritics } from 'normalize-diacritics/diacritics'
|
|||||||
export type SearchTextValue =
|
export type SearchTextValue =
|
||||||
string | number | boolean | bigint | null | undefined
|
string | number | boolean | bigint | null | undefined
|
||||||
|
|
||||||
|
const NON_ASCII = /[^\x00-\x7F]/
|
||||||
|
const SEPARATORS = /[^\p{L}\p{N}]+/u
|
||||||
|
|
||||||
export function normalizeSearchText(value: SearchTextValue): string {
|
export function normalizeSearchText(value: SearchTextValue): string {
|
||||||
const normalized = diacritics.reduce(
|
const text = String(value ?? '')
|
||||||
(text, replacement) => {
|
|
||||||
return text.replace(replacement.diacritics, replacement.letter)
|
// Nothing in the table matches ASCII, so skip normaliation
|
||||||
},
|
if (!NON_ASCII.test(text)) return text.toLocaleLowerCase()
|
||||||
String(value ?? '')
|
|
||||||
)
|
const normalized = diacritics.reduce((text, replacement) => {
|
||||||
|
return text.replace(replacement.diacritics, replacement.letter)
|
||||||
|
}, text)
|
||||||
|
|
||||||
return normalized.toLocaleLowerCase()
|
return normalized.toLocaleLowerCase()
|
||||||
}
|
}
|
||||||
@@ -18,8 +23,28 @@ export function matchesSearchText(
|
|||||||
value: SearchTextValue,
|
value: SearchTextValue,
|
||||||
searchText: SearchTextValue
|
searchText: SearchTextValue
|
||||||
): boolean {
|
): boolean {
|
||||||
const normalizedValue = normalizeSearchText(value)
|
const query = normalizeSearchText(searchText)
|
||||||
const searchTerms = normalizeSearchText(searchText).trim().split(/\s+/)
|
const terms = query.split(SEPARATORS).filter(Boolean)
|
||||||
|
|
||||||
return searchTerms.every((term) => normalizedValue.includes(term))
|
// Empty or punctuation-only query, nothing to split into terms
|
||||||
|
if (terms.length === 0) {
|
||||||
|
return normalizeSearchText(value).includes(query.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = normalizeSearchText(value).split(SEPARATORS).filter(Boolean)
|
||||||
|
const claimed = new Array<boolean>(words.length).fill(false)
|
||||||
|
|
||||||
|
// Each term takes a word of its own, longest first, so that "another tag th"
|
||||||
|
// doesn't match "Another Tag" by finding the "th" inside "another"
|
||||||
|
return terms
|
||||||
|
.sort((a, b) => b.length - a.length)
|
||||||
|
.every((term) => {
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
if (!claimed[i] && words[i].includes(term)) {
|
||||||
|
claimed[i] = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const environment = {
|
|||||||
apiVersion: '10', // match src/paperless/settings.py
|
apiVersion: '10', // match src/paperless/settings.py
|
||||||
appTitle: DEFAULT_APP_TITLE,
|
appTitle: DEFAULT_APP_TITLE,
|
||||||
tag: 'prod',
|
tag: 'prod',
|
||||||
version: '3.0.5',
|
version: '3.1.0',
|
||||||
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/',
|
||||||
|
|||||||
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2310
-1139
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2309
-1138
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2336
-1165
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2499
-1328
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+3555
-2385
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2529
-1359
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2344
-1173
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
+2309
-1138
File diff suppressed because it is too large
Load Diff
+2310
-1139
File diff suppressed because it is too large
Load Diff
+2307
-1136
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ from documents.models import DocumentType
|
|||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_objects
|
||||||
from documents.plugins.helpers import DocumentsStatusManager
|
from documents.plugins.helpers import DocumentsStatusManager
|
||||||
from documents.tasks import bulk_update_documents
|
from documents.tasks import bulk_update_documents
|
||||||
from documents.tasks import consume_file
|
from documents.tasks import consume_file
|
||||||
@@ -430,10 +430,13 @@ def set_permissions(
|
|||||||
else:
|
else:
|
||||||
qs.update(owner=owner)
|
qs.update(owner=owner)
|
||||||
|
|
||||||
for doc in qs:
|
|
||||||
set_permissions_for_object(permissions=set_permissions, object=doc, merge=merge)
|
|
||||||
|
|
||||||
affected_docs = list(qs.values_list("pk", flat=True))
|
affected_docs = list(qs.values_list("pk", flat=True))
|
||||||
|
set_permissions_for_objects(
|
||||||
|
permissions=set_permissions,
|
||||||
|
model=Document,
|
||||||
|
pks=affected_docs,
|
||||||
|
merge=merge,
|
||||||
|
)
|
||||||
|
|
||||||
bulk_update_documents.apply_async(
|
bulk_update_documents.apply_async(
|
||||||
kwargs={"document_ids": affected_docs},
|
kwargs={"document_ids": affected_docs},
|
||||||
|
|||||||
@@ -173,6 +173,184 @@ def set_permissions_for_object(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_permissions(codenames: set[str], ctype: ContentType) -> list[Permission]:
|
||||||
|
"""
|
||||||
|
Resolves `codenames` to Permission rows, raising like the single-object
|
||||||
|
assign_perm() this bulk path replaces does (via a `.get()` internally)
|
||||||
|
if any codename doesn't exist -- e.g. a client-supplied action name that
|
||||||
|
was never validated (BulkEditObjectsSerializer._validate_permissions
|
||||||
|
calls validate_set_permissions() only for its side-effecting id checks
|
||||||
|
and discards the filtered dict it returns, so an unrecognized action key
|
||||||
|
reaches this function as-is). A plain `.filter()` with no existence
|
||||||
|
check would otherwise silently build zero rows and no-op instead of
|
||||||
|
reporting the bad input.
|
||||||
|
"""
|
||||||
|
permission_objs = list(
|
||||||
|
Permission.objects.filter(content_type=ctype, codename__in=codenames),
|
||||||
|
)
|
||||||
|
missing = codenames - {p.codename for p in permission_objs}
|
||||||
|
if missing:
|
||||||
|
raise Permission.DoesNotExist(
|
||||||
|
f"Permission matching query does not exist for codename(s): "
|
||||||
|
f"{', '.join(sorted(missing))}",
|
||||||
|
)
|
||||||
|
return permission_objs
|
||||||
|
|
||||||
|
|
||||||
|
# Target number of permission rows to build in Python before handing them to
|
||||||
|
# bulk_create -- keeps peak memory bounded for a large "apply to all" call,
|
||||||
|
# independent of bulk_create's own batch_size (which only caps the size of
|
||||||
|
# each INSERT statement, not how many row objects exist in memory at once).
|
||||||
|
_PERMISSION_ROW_CHUNK_SIZE = 5000
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_bulk_permission_entry(
|
||||||
|
*,
|
||||||
|
perm_model: type[UserObjectPermission] | type[GroupObjectPermission],
|
||||||
|
identity_model: type[User] | type[Group],
|
||||||
|
identity_field: str,
|
||||||
|
ids: list[int],
|
||||||
|
codename: str,
|
||||||
|
permission_objs: list[Permission],
|
||||||
|
ctype: ContentType,
|
||||||
|
object_pks: list[str],
|
||||||
|
merge: bool,
|
||||||
|
) -> None:
|
||||||
|
# Only the ids are needed to build permission rows (via `<field>_id=`),
|
||||||
|
# so avoid fetching full User/Group rows for identities that may not
|
||||||
|
# even end up being granted anything new.
|
||||||
|
add_ids = set(
|
||||||
|
identity_model.objects.filter(id__in=ids).values_list("id", flat=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not merge:
|
||||||
|
existing_ids = set(
|
||||||
|
perm_model.objects.filter(
|
||||||
|
content_type=ctype,
|
||||||
|
object_pk__in=object_pks,
|
||||||
|
permission__codename=codename,
|
||||||
|
)
|
||||||
|
.values_list(f"{identity_field}_id", flat=True)
|
||||||
|
.distinct(),
|
||||||
|
)
|
||||||
|
remove_ids = existing_ids - add_ids
|
||||||
|
if remove_ids:
|
||||||
|
perm_model.objects.filter(
|
||||||
|
content_type=ctype,
|
||||||
|
object_pk__in=object_pks,
|
||||||
|
permission__codename=codename,
|
||||||
|
**{f"{identity_field}_id__in": remove_ids},
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
if not add_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
rows_per_pk = len(permission_objs) * len(add_ids)
|
||||||
|
pks_per_chunk = max(1, _PERMISSION_ROW_CHUNK_SIZE // rows_per_pk)
|
||||||
|
for start in range(0, len(object_pks), pks_per_chunk):
|
||||||
|
pk_chunk = object_pks[start : start + pks_per_chunk]
|
||||||
|
rows = [
|
||||||
|
perm_model(
|
||||||
|
content_type=ctype,
|
||||||
|
object_pk=pk,
|
||||||
|
permission=permission_obj,
|
||||||
|
**{f"{identity_field}_id": identity_id},
|
||||||
|
)
|
||||||
|
for permission_obj in permission_objs
|
||||||
|
for pk in pk_chunk
|
||||||
|
for identity_id in add_ids
|
||||||
|
]
|
||||||
|
# ignore_conflicts skips only rows that already exist as an exact
|
||||||
|
# (identity, permission, object) match -- the same de-dup the
|
||||||
|
# underlying (user|group, permission, object_pk) unique constraint
|
||||||
|
# already enforces for the single-object assign_perm() this
|
||||||
|
# replaces, so it doesn't change what counts as "already granted".
|
||||||
|
# batch_size caps how many rows go into a single INSERT so a huge
|
||||||
|
# chunk doesn't build one enormous statement.
|
||||||
|
perm_model.objects.bulk_create(rows, ignore_conflicts=True, batch_size=1000)
|
||||||
|
|
||||||
|
|
||||||
|
def set_permissions_for_objects(
|
||||||
|
permissions: dict,
|
||||||
|
model: type[Model],
|
||||||
|
pks: QuerySet | list,
|
||||||
|
*,
|
||||||
|
merge: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Bulk equivalent of set_permissions_for_object: applies the same
|
||||||
|
permission changes to every object identified by `pks` at once.
|
||||||
|
|
||||||
|
Takes a model + pks (rather than model instances) deliberately -- the
|
||||||
|
permission rows built below only ever need `pk`, `content_type`, and
|
||||||
|
identity ids, so callers shouldn't have to fetch full rows (with every
|
||||||
|
other field) just to hand them to this function.
|
||||||
|
|
||||||
|
Deliberately does not use guardian's queryset/list-aware assign_perm:
|
||||||
|
passing a list as the object routes to bulk_assign_perm, which skips
|
||||||
|
creating a direct permission row for anyone who already has the
|
||||||
|
permission via ANY group membership (it checks
|
||||||
|
ObjectPermissionChecker.has_perm, which is group-inheritance-aware) --
|
||||||
|
unlike the single-object assign_perm this replaces, which always
|
||||||
|
ensures a direct row via get_or_create regardless of group-derived
|
||||||
|
access. Losing that guarantee would mean a later revocation of the
|
||||||
|
group's grant silently strips access an admin explicitly asked to be
|
||||||
|
direct. Bulk-creating rows straight against the permission models
|
||||||
|
instead (see _apply_bulk_permission_entry) preserves the original
|
||||||
|
always-create-a-direct-row semantics while still batching every object
|
||||||
|
and every identity into one query per action, rather than one query per
|
||||||
|
(object, user) pair.
|
||||||
|
"""
|
||||||
|
object_pks = [str(pk) for pk in pks]
|
||||||
|
if not object_pks: # pragma: no cover
|
||||||
|
return
|
||||||
|
|
||||||
|
model_name = model.__name__.lower()
|
||||||
|
ctype = ContentType.objects.get_for_model(model)
|
||||||
|
|
||||||
|
for action, entry in permissions.items():
|
||||||
|
codename = f"{action}_{model_name}"
|
||||||
|
implied_codenames = {codename}
|
||||||
|
if action == "change":
|
||||||
|
# change gives view too
|
||||||
|
implied_codenames.add(f"view_{model_name}")
|
||||||
|
|
||||||
|
# Resolved once per action (not once per users/groups branch) and
|
||||||
|
# shared between both below -- also where an unrecognized action
|
||||||
|
# name (see _resolve_permissions) is caught.
|
||||||
|
permission_objs = (
|
||||||
|
_resolve_permissions(implied_codenames, ctype)
|
||||||
|
if "users" in entry or "groups" in entry
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
|
if "users" in entry:
|
||||||
|
_apply_bulk_permission_entry(
|
||||||
|
perm_model=UserObjectPermission,
|
||||||
|
identity_model=User,
|
||||||
|
identity_field="user",
|
||||||
|
ids=entry["users"],
|
||||||
|
codename=codename,
|
||||||
|
permission_objs=permission_objs,
|
||||||
|
ctype=ctype,
|
||||||
|
object_pks=object_pks,
|
||||||
|
merge=merge,
|
||||||
|
)
|
||||||
|
|
||||||
|
if "groups" in entry:
|
||||||
|
_apply_bulk_permission_entry(
|
||||||
|
perm_model=GroupObjectPermission,
|
||||||
|
identity_model=Group,
|
||||||
|
identity_field="group",
|
||||||
|
ids=entry["groups"],
|
||||||
|
codename=codename,
|
||||||
|
permission_objs=permission_objs,
|
||||||
|
ctype=ctype,
|
||||||
|
object_pks=object_pks,
|
||||||
|
merge=merge,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def permitted_object_ids(
|
def permitted_object_ids(
|
||||||
user: User | None,
|
user: User | None,
|
||||||
model: type[Model],
|
model: type[Model],
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
|
from guardian.shortcuts import get_groups_with_perms
|
||||||
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@@ -815,6 +818,47 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(StoragePath.objects.count(), 0)
|
self.assertEqual(StoragePath.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_bulk_objects_set_permissions_batched_across_object_count(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Many tags are being bulk-edited to set permissions at once
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with set_permissions
|
||||||
|
operation over a small batch vs. a much larger one
|
||||||
|
THEN:
|
||||||
|
- Permissions are applied correctly at both scales
|
||||||
|
"""
|
||||||
|
group1 = Group.objects.create(name="perm-group")
|
||||||
|
permissions = {
|
||||||
|
"view": {"users": [self.user1.id, self.user2.id], "groups": [group1.id]},
|
||||||
|
"change": {"users": [self.user1.id], "groups": [group1.id]},
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_with_n_tags(n: int) -> None:
|
||||||
|
tags = [Tag.objects.create(name=f"perm-tag-{n}-{i}") for i in range(n)]
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [t.id for t in tags],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
|
"permissions": permissions,
|
||||||
|
"merge": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
for tag in tags:
|
||||||
|
self.assertEqual(get_users_with_perms(tag).count(), 2)
|
||||||
|
self.assertEqual(get_groups_with_perms(tag).count(), 1)
|
||||||
|
|
||||||
|
run_with_n_tags(5)
|
||||||
|
run_with_n_tags(50)
|
||||||
|
|
||||||
def test_bulk_objects_delete_all_filtered(self) -> None:
|
def test_bulk_objects_delete_all_filtered(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
@@ -19,6 +20,7 @@ from documents.models import Document
|
|||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
|
from documents.permissions import set_permissions_for_objects
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -510,6 +512,120 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(groups_with_perms.count(), 2)
|
self.assertEqual(groups_with_perms.count(), 2)
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.bulk_update_documents.apply_async")
|
||||||
|
def test_set_permissions_batched_across_document_count(
|
||||||
|
self,
|
||||||
|
m,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Many documents are being bulk-edited to set permissions at once
|
||||||
|
WHEN:
|
||||||
|
- set_permissions runs over a small batch vs. a much larger one
|
||||||
|
THEN:
|
||||||
|
- Permissions are applied correctly at both scales
|
||||||
|
"""
|
||||||
|
permissions = {
|
||||||
|
"view": {
|
||||||
|
"users": [self.user1.id, self.user2.id],
|
||||||
|
"groups": [self.group2.id],
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [self.user1.id],
|
||||||
|
"groups": [self.group2.id],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_with_n_documents(n: int) -> None:
|
||||||
|
docs = [
|
||||||
|
Document.objects.create(checksum=f"perm-{n}-{i}", title=f"perm-{n}-{i}")
|
||||||
|
for i in range(n)
|
||||||
|
]
|
||||||
|
bulk_edit.set_permissions(
|
||||||
|
[doc.id for doc in docs],
|
||||||
|
set_permissions=permissions,
|
||||||
|
owner=self.owner,
|
||||||
|
merge=False,
|
||||||
|
)
|
||||||
|
for doc in docs:
|
||||||
|
self.assertEqual(get_users_with_perms(doc).count(), 2)
|
||||||
|
self.assertEqual(get_groups_with_perms(doc).count(), 1)
|
||||||
|
|
||||||
|
run_with_n_documents(5)
|
||||||
|
run_with_n_documents(50)
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.bulk_update_documents.apply_async")
|
||||||
|
def test_set_permissions_grants_direct_perm_even_if_already_granted_via_group(
|
||||||
|
self,
|
||||||
|
m,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A user already has view access to a document via group
|
||||||
|
membership, with no direct grant of their own
|
||||||
|
WHEN:
|
||||||
|
- set_permissions explicitly grants that same user direct view
|
||||||
|
access via bulk_edit
|
||||||
|
THEN:
|
||||||
|
- A direct permission grant is created for the user, not skipped
|
||||||
|
because they already have equivalent access via the group
|
||||||
|
|
||||||
|
Regression test: guardian's queryset-aware assign_perm() (routed to
|
||||||
|
when the target is a list/queryset) skips creating a direct row for
|
||||||
|
anyone whose ObjectPermissionChecker.has_perm() already returns True
|
||||||
|
-- which includes group-derived access. The single-object assign_perm
|
||||||
|
this bulk path replaces has no such check; it always ensures a
|
||||||
|
direct row via get_or_create. Losing that guarantee would mean
|
||||||
|
revoking the group's grant later silently strips access that was
|
||||||
|
supposed to be explicit.
|
||||||
|
"""
|
||||||
|
self.doc1.owner = self.user1
|
||||||
|
self.doc1.save()
|
||||||
|
self.user1.groups.add(self.group1)
|
||||||
|
assign_perm("view_document", self.group1, self.doc1)
|
||||||
|
|
||||||
|
bulk_edit.set_permissions(
|
||||||
|
[self.doc1.id],
|
||||||
|
set_permissions={
|
||||||
|
"view": {"users": [self.user1.id], "groups": []},
|
||||||
|
},
|
||||||
|
merge=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
direct_users = get_users_with_perms(
|
||||||
|
self.doc1,
|
||||||
|
only_with_perms_in=["view_document"],
|
||||||
|
with_group_users=False,
|
||||||
|
)
|
||||||
|
self.assertIn(self.user1, direct_users)
|
||||||
|
|
||||||
|
def test_set_permissions_for_objects_raises_for_unknown_action(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An unrecognized permission action name with users to grant it
|
||||||
|
to
|
||||||
|
WHEN:
|
||||||
|
- set_permissions_for_objects is called
|
||||||
|
THEN:
|
||||||
|
- Permission.DoesNotExist is raised, not a silent no-op
|
||||||
|
|
||||||
|
Regression test: the endpoint that calls this
|
||||||
|
(BulkEditObjectPermissionsView) never actually validates action
|
||||||
|
names against the raw client-supplied permissions dict --
|
||||||
|
BulkEditObjectsSerializer._validate_permissions calls
|
||||||
|
validate_set_permissions() only for its side-effecting user/group id
|
||||||
|
checks and discards the filtered dict it returns -- so a bogus
|
||||||
|
action key reaches this function as-is. Resolving the Permission via
|
||||||
|
a bare `.filter()` (which returns empty instead of raising) would
|
||||||
|
silently drop the grant and report success.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(Permission.DoesNotExist):
|
||||||
|
set_permissions_for_objects(
|
||||||
|
{"not_a_real_action": {"users": [self.user1.id], "groups": []}},
|
||||||
|
Document,
|
||||||
|
[self.doc1.pk],
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch("documents.models.Document.delete")
|
@mock.patch("documents.models.Document.delete")
|
||||||
def test_delete_documents_old_uuid_field(self, m) -> None:
|
def test_delete_documents_old_uuid_field(self, m) -> None:
|
||||||
m.side_effect = Exception("Data too long for column 'transaction_id' at row 1")
|
m.side_effect = Exception("Data too long for column 'transaction_id' at row 1")
|
||||||
|
|||||||
@@ -5573,6 +5573,41 @@ class TestApplyAISuggestionsWorkflowAction(
|
|||||||
self.doc.refresh_from_db()
|
self.doc.refresh_from_db()
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
def test_fields_persist_when_tags_are_applied_in_the_same_run(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A document that already has a filename, as any consumed document does
|
||||||
|
- Suggestions carrying both a document type and tags
|
||||||
|
WHEN:
|
||||||
|
- The suggestions are applied
|
||||||
|
THEN:
|
||||||
|
- The document type is still set after the tags are added
|
||||||
|
|
||||||
|
Adding tags fires m2m_changed, and update_filename_and_move_files
|
||||||
|
refreshes the document from the database. Assigning fields and then
|
||||||
|
adding tags before saving loses those assignments, and only for
|
||||||
|
documents with a filename, so it does not reproduce on a bare
|
||||||
|
Document.objects.create().
|
||||||
|
"""
|
||||||
|
self.doc.filename = "originals/original.pdf"
|
||||||
|
self.doc.save(update_fields=["filename"])
|
||||||
|
|
||||||
|
action = self.make_action(ai_create_missing=True)
|
||||||
|
changed = self.apply(action)
|
||||||
|
|
||||||
|
self.assertIn("document_type", changed)
|
||||||
|
self.assertIn("tags", changed)
|
||||||
|
self.assertIsNotNone(
|
||||||
|
self.doc.document_type,
|
||||||
|
"document_type was reported as applied but did not persist",
|
||||||
|
)
|
||||||
|
self.assertEqual(self.doc.document_type.name, "Suggested Document Type")
|
||||||
|
self.assertEqual(self.doc.correspondent.name, "Existing Correspondent")
|
||||||
|
self.assertCountEqual(
|
||||||
|
[t.name for t in self.doc.tags.all()],
|
||||||
|
["Existing Tag", "Suggested Tag"],
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_added_trigger_queues_task(self) -> None:
|
def test_document_added_trigger_queues_task(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ from documents.permissions import has_perms_owner_aware
|
|||||||
from documents.permissions import has_system_status_permission
|
from documents.permissions import has_system_status_permission
|
||||||
from documents.permissions import permitted_document_ids
|
from documents.permissions import permitted_document_ids
|
||||||
from documents.permissions import permitted_object_ids
|
from documents.permissions import permitted_object_ids
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_objects
|
||||||
from documents.plugins.date_parsing import get_date_parser
|
from documents.plugins.date_parsing import get_date_parser
|
||||||
from documents.schema import generate_object_with_permissions_schema
|
from documents.schema import generate_object_with_permissions_schema
|
||||||
from documents.search import SearchHit
|
from documents.search import SearchHit
|
||||||
@@ -4914,12 +4914,12 @@ class BulkEditObjectsView(PassUserMixin):
|
|||||||
qs_owner_update.update(owner=owner)
|
qs_owner_update.update(owner=owner)
|
||||||
|
|
||||||
if "permissions" in serializer.validated_data:
|
if "permissions" in serializer.validated_data:
|
||||||
for obj in qs:
|
set_permissions_for_objects(
|
||||||
set_permissions_for_object(
|
permissions=permissions,
|
||||||
permissions=permissions,
|
model=object_class,
|
||||||
object=obj,
|
pks=qs.values_list("pk", flat=True),
|
||||||
merge=merge,
|
merge=merge,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|||||||
@@ -226,20 +226,20 @@ def apply_ai_suggestions_to_document(
|
|||||||
document.created = created
|
document.created = created
|
||||||
updated_fields.append("created")
|
updated_fields.append("created")
|
||||||
|
|
||||||
|
tags_to_add: list[Tag] = []
|
||||||
if AISuggestionField.TAGS in selected:
|
if AISuggestionField.TAGS in selected:
|
||||||
choice = suggestions["tags"]
|
choice = suggestions["tags"]
|
||||||
names = choice["new_names"]
|
names = choice["new_names"]
|
||||||
tags = resolve_tags(
|
tags_to_add = resolve_tags(
|
||||||
names,
|
names,
|
||||||
resolve_tag_ids(choice["existing_ids"], owner)
|
resolve_tag_ids(choice["existing_ids"], owner)
|
||||||
+ match_tags_by_name(names, owner),
|
+ match_tags_by_name(names, owner),
|
||||||
create_missing=create_missing,
|
create_missing=create_missing,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
if tags:
|
if tags_to_add:
|
||||||
# Suggested tags are always added, so overwrite_existing
|
# Suggested tags are always added, so overwrite_existing
|
||||||
# does not really apply here
|
# does not really apply here
|
||||||
document.add_nested_tags(tags)
|
|
||||||
updated_fields.append("tags")
|
updated_fields.append("tags")
|
||||||
|
|
||||||
if updated_fields:
|
if updated_fields:
|
||||||
@@ -249,6 +249,10 @@ def apply_ai_suggestions_to_document(
|
|||||||
]
|
]
|
||||||
document.save(update_fields=[*direct_updated_fields, "modified"])
|
document.save(update_fields=[*direct_updated_fields, "modified"])
|
||||||
|
|
||||||
|
# Tags at the end so m2m_changed doesn't trigger db and overwrite other changes
|
||||||
|
if tags_to_add:
|
||||||
|
document.add_nested_tags(tags_to_add)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Applied AI suggestions %s to document %s",
|
"Applied AI suggestions %s to document %s",
|
||||||
updated_fields or "(none)",
|
updated_fields or "(none)",
|
||||||
|
|||||||
+1078
-782
File diff suppressed because it is too large
Load Diff
+1086
-790
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1063
-767
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1062
-766
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1078
-782
File diff suppressed because it is too large
Load Diff
+1086
-790
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user