Compare commits

..
81 Commits
Author SHA1 Message Date
Crowdin Bot 3ad4ed30d1 New Crowdin translations by GitHub Action 2026-10-02 12:32:27 +00:00
GitHub Actions f1ae9c00ac Auto translate strings 2026-10-02 05:59:48 +00:00
dependabot[bot] bc2ff953ed Chore(deps): Bump the frontend-angular-dependencies group (#14329)
Bumps the frontend-angular-dependencies group in /src-ui with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `22.1.7` | `22.2.0` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `22.1.7` | `22.2.0` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `22.1.7` | `22.2.0` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `22.1.7` | `22.2.0` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `22.1.7` | `22.2.0` |
| [@angular/localize](https://github.com/angular/angular) | `22.1.7` | `22.2.0` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `22.1.7` | `22.2.0` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `22.1.7` | `22.2.0` |
| [@angular-devkit/core](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular-devkit/schematics](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular/build](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular/cli](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `22.1.7` | `22.2.0` |


Updates `@angular/cdk` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/v22.1.7...v22.2.0)

Updates `@angular/common` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/common)

Updates `@angular/compiler` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/compiler)

Updates `@angular/core` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/core)

Updates `@angular/forms` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/forms)

Updates `@angular/localize` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/compare/v22.1.7...v22.2.0)

Updates `@angular/platform-browser` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/platform-browser)

Updates `@angular/router` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/router)

Updates `@angular-devkit/core` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular-devkit/schematics` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular/build` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular/cli` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular/compiler-cli` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/core"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/schematics"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/build"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 05:58:14 +00:00
dependabot[bot] e17689a91b Chore(deps-dev): Bump prettier from 3.9.8 to 3.9.9 in /src-ui (#14331)
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.8 to 3.9.9.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.8...3.9.9)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 05:42:48 +00:00
dependabot[bot] 764d1a492d Chore(deps-dev): Bump the frontend-eslint-dependencies group (#14330)
Bumps the frontend-eslint-dependencies group in /src-ui with 3 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils).


Updates `@typescript-eslint/eslint-plugin` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/parser)

Updates `@typescript-eslint/utils` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/utils)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 05:31:40 +00:00
9d50a3b261 Fix: ship pdf.js CMaps so CJK documents render in the viewer (#14318)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-10-02 03:27:15 +00:00
dependabot[bot] da3de299fc Chore(deps-dev): Bump zensical (#14326)
Bumps the development group with 1 update: [zensical](https://github.com/zensical/zensical).


Updates `zensical` from 0.0.64 to 0.0.65
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.64...v0.0.65)

---
updated-dependencies:
- dependency-name: zensical
  dependency-version: 0.0.65
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 20:32:40 +00:00
GitHub Actions 3a022e2dc4 Auto translate strings 2026-10-01 20:02:19 +00:00
shamoon 58bda48682 Enhancement: more control over suggestion requests (#14258) 2026-10-01 20:00:52 +00:00
dependabot[bot] b2a2e15ead Chore(deps): Bump the uv group across 1 directory with 2 updates (#14314)
Bumps the uv group with 2 updates in the / directory: [pyjwt](https://github.com/jpadilla/pyjwt) and [urllib3](https://github.com/urllib3/urllib3).


Updates `pyjwt` from 2.13.0 to 2.15.0
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.13.0...2.15.0)

Updates `urllib3` from 2.7.0 to 2.8.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.7.0...2.8.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.15.0
  dependency-type: indirect
- dependency-name: urllib3
  dependency-version: 2.8.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 17:14:42 +00:00
shamoon e865185425 Chore: anchor admin url pattern (#14316) 2026-10-01 01:22:06 -07:00
shamoon d02c19a09e Chorehancement: set manifest CORS for credentials (#14307) 2026-09-30 08:03:04 -07:00
dependabot[bot]andstumpylog 4a106454ce Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates (#14305)
* Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates

Bumps the utilities-minor group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [django-guardian](https://github.com/django-guardian/django-guardian) | `3.4.1` | `3.5.0` |
| [flower](https://github.com/mher/flower) | `2.1.0` | `2.2.0` |
| [sentence-transformers](https://github.com/huggingface/sentence-transformers) | `6.0.1` | `6.1.0` |
| [watchfiles](https://github.com/samuelcolvin/watchfiles) | `1.2.0` | `1.3.0` |
| [mysqlclient](https://github.com/PyMySQL/mysqlclient) | `2.2.8` | `2.3.0` |
| [faker](https://github.com/joke2k/faker) | `40.38.0` | `40.39.0` |
| [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) | `16.6.1` | `16.7` |



Updates `django-guardian` from 3.4.1 to 3.5.0
- [Release notes](https://github.com/django-guardian/django-guardian/releases)
- [Commits](https://github.com/django-guardian/django-guardian/compare/3.4.1...3.5.0)

Updates `flower` from 2.1.0 to 2.2.0
- [Release notes](https://github.com/mher/flower/releases)
- [Commits](https://github.com/mher/flower/compare/v2.1.0...v2.2.0)

Updates `sentence-transformers` from 6.0.1 to 6.1.0
- [Release notes](https://github.com/huggingface/sentence-transformers/releases)
- [Commits](https://github.com/huggingface/sentence-transformers/compare/v6.0.1...v6.1.0)

Updates `watchfiles` from 1.2.0 to 1.3.0
- [Release notes](https://github.com/samuelcolvin/watchfiles/releases)
- [Commits](https://github.com/samuelcolvin/watchfiles/compare/v1.2.0...v1.3.0)

Updates `mysqlclient` from 2.2.8 to 2.3.0
- [Release notes](https://github.com/PyMySQL/mysqlclient/releases)
- [Changelog](https://github.com/PyMySQL/mysqlclient/blob/main/HISTORY.rst)
- [Commits](https://github.com/PyMySQL/mysqlclient/compare/v2.2.8...v2.3.0)

Updates `faker` from 40.38.0 to 40.39.0
- [Release notes](https://github.com/joke2k/faker/releases)
- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/joke2k/faker/compare/v40.38.0...v40.39.0)

Updates `pytest-rerunfailures` from 16.6.1 to 16.7
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/16.6.1...16.7)

---
updated-dependencies:
- dependency-name: django-guardian
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: faker
  dependency-version: 40.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: flower
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: mysqlclient
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: pytest-rerunfailures
  dependency-version: '16.7'
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: sentence-transformers
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: watchfiles
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Linting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stumpylog <797416+stumpylog@users.noreply.github.com>
2026-09-29 13:28:35 -07:00
dependabot[bot] 4254100fae docker-compose(deps): bump greenmail/standalone in /docker/compose (#14281)
Bumps greenmail/standalone from 2.1.13 to 2.1.14.

---
updated-dependencies:
- dependency-name: greenmail/standalone
  dependency-version: 2.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 11:31:43 -07:00
dependabot[bot] 74f727b1f3 docker(deps): Bump astral-sh/uv (#14282)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.12.16-python3.14-trixie-slim to 0.12.20-python3.14-trixie-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.12.16...0.12.20)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.12.19-python3.14-trixie-slim
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 10:08:39 -07:00
dependabot[bot] fb6fad952e Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 3 updates (#14283)
Bumps the pre-commit-dependencies group with 3 updates in the / directory: [https://github.com/rbubley/mirrors-prettier](https://github.com/rbubley/mirrors-prettier), prettier and [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit).


Updates `https://github.com/rbubley/mirrors-prettier` from v3.9.6 to 3.9.8
- [Commits](https://github.com/rbubley/mirrors-prettier/compare/v3.9.6...v3.9.8)

Updates `prettier` from 3.9.6 to 3.9.8

Updates `https://github.com/astral-sh/ruff-pre-commit` from v0.16.7 to 0.16.8
- [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases)
- [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.7...v0.16.8)

---
updated-dependencies:
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.16.8
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/rbubley/mirrors-prettier
  dependency-version: 3.9.8
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: prettier
  dependency-version: 3.9.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pre-commit-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 09:33:49 -07:00
dependabot[bot] f9b4e0c8e0 Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates (#14297)
Bumps the utilities-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [llama-index-core](https://github.com/run-llama/llama_index) | `0.14.24` | `0.14.25` |
| [tantivy](https://github.com/quickwit-oss/tantivy-py) | `0.26.0` | `0.26.2` |
| [psycopg-pool](https://github.com/psycopg/psycopg) | `3.3.1` | `3.3.2` |
| [zensical](https://github.com/zensical/zensical) | `0.0.60` | `0.0.64` |
| [prek](https://github.com/j178/prek) | `0.5.2` | `0.5.3` |
| [ruff](https://github.com/astral-sh/ruff) | `0.16.7` | `0.16.8` |



Updates `llama-index-core` from 0.14.24 to 0.14.25
- [Release notes](https://github.com/run-llama/llama_index/releases)
- [Changelog](https://github.com/run-llama/llama_index/blob/main/CHANGELOG.md)
- [Commits](https://github.com/run-llama/llama_index/compare/v0.14.24...v0.14.25)

Updates `tantivy` from 0.26.0 to 0.26.2
- [Release notes](https://github.com/quickwit-oss/tantivy-py/releases)
- [Commits](https://github.com/quickwit-oss/tantivy-py/compare/0.26.0...0.26.2)

Updates `psycopg-pool` from 3.3.1 to 3.3.2
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.1...3.3.2)

Updates `zensical` from 0.0.60 to 0.0.64
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.60...v0.0.64)

Updates `prek` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.5.2...v0.5.3)

Updates `ruff` from 0.16.7 to 0.16.8
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.7...0.16.8)

---
updated-dependencies:
- dependency-name: llama-index-core
  dependency-version: 0.14.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: prek
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: psycopg-pool
  dependency-version: 3.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: ruff
  dependency-version: 0.16.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: tantivy
  dependency-version: 0.26.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: zensical
  dependency-version: 0.0.63
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 08:52:09 -07:00
dependabot[bot] e5041097af Chore(deps): Bump the actions group across 1 directory with 10 updates (#14301)
Bumps the actions group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `10.1.0` | `10.2.0` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `7.0.0` | `7.1.1` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.3.0` | `4.4.1` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.3.0` | `7.4.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.38.0` | `4.38.1` |
| [stumpylog/image-cleaner-action/ephemeral](https://github.com/stumpylog/image-cleaner-action) | `0.12.0` | `0.13.0` |
| [stumpylog/image-cleaner-action/untagged](https://github.com/stumpylog/image-cleaner-action) | `0.12.0` | `0.13.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.0` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.0` | `4.38.1` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `3.0.2` | `3.2.0` |



Updates `astral-sh/setup-uv` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/bec219d24cd3e171d82865faccec33120bb574f4...c18668ad3cf93ea998bef934396af7bb5c839dc7)

Updates `codecov/codecov-action` from 7.0.0 to 7.1.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/fb8b3582c8e4def4969c97caa2f19720cb33a72f...303a32d7a59b442fa8d48b6a1cc6825c09c847a5)

Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.1
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...f87e5991a6d7451dcb8d9637bfbc97413f497069)

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

Updates `github/codeql-action/upload-sarif` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `stumpylog/image-cleaner-action/ephemeral` from 0.12.0 to 0.13.0
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/4fe057d991d63b8f6d5d22c40f17c1bca2226537...21f875bab2376314e0525c614e923c669434f0e4)

Updates `stumpylog/image-cleaner-action/untagged` from 0.12.0 to 0.13.0
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/4fe057d991d63b8f6d5d22c40f17c1bca2226537...21f875bab2376314e0525c614e923c669434f0e4)

Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `crowdin/github-action` from 3.0.2 to 3.2.0
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/0d5670f539973aea2f01abce61a8989934df0025...df474cdfb9f41d6ae777118749477c2cdf7cacc8)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: stumpylog/image-cleaner-action/ephemeral
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: stumpylog/image-cleaner-action/untagged
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: crowdin/github-action
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 08:18:28 -07:00
Trenton H 61f36eb253 Chore: Disallow prerelease resolution in uv (#14296)
uv's default "if-necessary" prerelease policy will silently select a
prerelease package when no stable release satisfies the combined
constraint set, even without an explicit opt-in.  Disallow pre-releases
instead
2026-09-28 18:49:21 +00:00
Trenton H e50bcc2efb Chore: Updates typing baseline files (#14295)
* Chore: Refresh the mypy and pyrefly baselines

* Chore: Type factory calls as the model they build for mypy

factory-boy leaves Factory() unannotated, so mypy took UserFactory() to
be a factory instance rather than a User. A shared TypedModelFactory
base declares the call as returning the model for type checking only.

* Refresh again after rebase
2026-09-28 11:32:05 -07:00
dependabot[bot] 88000aab45 Chore(deps-dev): Bump the frontend-jest-dependencies group (#14286)
Bumps the frontend-jest-dependencies group in /src-ui with 2 updates: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) and [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom).


Updates `jest` from 30.5.1 to 30.5.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest)

Updates `jest-environment-jsdom` from 30.5.1 to 30.5.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
- dependency-name: jest-environment-jsdom
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 09:22:21 +00:00
dependabot[bot] 01b4ab392c Chore(deps-dev): Bump eslint (#14287)
Bumps the frontend-eslint-dependencies group in /src-ui with 1 update: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.10.0 to 10.11.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.10.0...v10.11.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-eslint-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 09:11:55 +00:00
dependabot[bot] 52aa8758e3 Chore(deps-dev): Bump @types/node from 26.5.0 to 26.6.2 in /src-ui (#14288)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.5.0 to 26.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 09:00:39 +00:00
dependabot[bot] 4b7cc1c0c3 Chore(deps-dev): Bump prettier from 3.9.6 to 3.9.8 in /src-ui (#14289)
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.6 to 3.9.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 08:48:03 +00:00
GitHub Actions f02d7455be Auto translate strings 2026-09-28 08:32:00 +00:00
dependabot[bot] a61c35fcd8 Chore(deps): Bump the frontend-angular-dependencies group (#14285)
Bumps the frontend-angular-dependencies group in /src-ui with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `22.1.6` | `22.1.7` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `22.1.6` | `22.1.7` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `22.1.6` | `22.1.7` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `22.1.6` | `22.1.7` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `22.1.6` | `22.1.7` |
| [@angular/localize](https://github.com/angular/angular) | `22.1.6` | `22.1.7` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `22.1.6` | `22.1.7` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `22.1.6` | `22.1.7` |
| [@ng-select/ng-select](https://github.com/ng-select/ng-select) | `24.1.1` | `24.1.2` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `22.1.6` | `22.1.7` |


Updates `@angular/cdk` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/v22.1.6...v22.1.7)

Updates `@angular/common` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/common)

Updates `@angular/compiler` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/compiler)

Updates `@angular/core` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/core)

Updates `@angular/forms` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/forms)

Updates `@angular/localize` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/compare/v22.1.6...v22.1.7)

Updates `@angular/platform-browser` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/platform-browser)

Updates `@angular/router` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/router)

Updates `@ng-select/ng-select` from 24.1.1 to 24.1.2
- [Release notes](https://github.com/ng-select/ng-select/releases)
- [Commits](https://github.com/ng-select/ng-select/compare/v24.1.1...v24.1.2)

Updates `@angular/compiler-cli` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-select/ng-select"
  dependency-version: 24.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 22.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 08:30:31 +00:00
GitHub Actions 64552d5d5b Auto translate strings 2026-09-28 06:27:28 +00:00
shamoon a59edf3e5b Fix: use version page_count for versioned document (#14280) 2026-09-27 23:26:00 -07:00
Bernát Gábor 6a5d06ee26 Chore: replace bleach with turbohtml (#14269)
bleach's repository is archived, and it depends on html5lib, which has not had
a release since 2020. turbohtml provides a bleach-compatible clean and a native
linkify that runs in linear time, so the 2048-character cap on email
linkification from #13187 is no longer needed and long messages get their
email addresses linked again.
2026-09-27 16:50:19 -07:00
GitHub Actions 126ec414a8 Auto translate strings 2026-09-27 14:08:00 +00:00
shamoon 773744d04b Enhancement: include Django admin with 2FA (#14270) 2026-09-27 07:06:26 -07:00
shamoon 7b98085b31 Fix: prevent dev pdf viewer link flash (#14268) 2026-09-26 07:02:49 -07:00
shamoon b3bf220f56 Fix: rest of PDF link fix 2026-09-25 20:08:36 -07:00
shamoon d4429c3cc7 Fix: allow pointer events for pdf links in pngx viewer (#14264) 2026-09-25 18:35:21 -07:00
Trenton H 4f777d7438 Feature: propagate resolved secrets to interactive container shells (#14254)
docker exec bash bypasses s6, so *_FILE secrets resolved by init-env-file
into /run/s6/container_environment aren't visible even though s6 services
and management wrappers already see them via with-contenv.

Add /etc/profile.d/contenv.sh, sourced by both login and non-login
interactive bash shells, to export those resolved values into the shell.
2026-09-24 13:07:41 -07:00
Trenton H 2a44d8b5ba Fix: During a move to the trash directory, attempt to copy metadata, but don't let it fail the move (#14250) 2026-09-23 21:51:22 -07:00
Trenton H abf5050ea7 Fix: convert file mtime to the configured time zone directly (#14249)
The `created` date fallback derived a naive datetime from a file's
mtime using the OS-local zone, then labeled it as the configured
TIME_ZONE without converting. When the OS-local zone and TIME_ZONE
disagree, or when the C library can't resolve zoneinfo at all (as in
some sandboxed environments, where it silently falls back to UTC),
the resulting date can land on the wrong calendar day.

Convert the timestamp directly into the target zone with `tz=` on
fromtimestamp() instead of a naive conversion plus make_aware().
2026-09-23 15:00:48 -07:00
GitHub Actions 091ddf7c45 Auto translate strings 2026-09-23 19:13:55 +00:00
shamoon c9f7f2cfbe Fix: ensure documentDeleted subscription is discarded (#14247) 2026-09-23 12:12:26 -07:00
Trenton H b457610ffb Chore: Fix bugs in the test suite (#14244)
* Fix: redirect SHARE_LINK_BUNDLE_DIR to the test temp layout instead of the real media directory

* Fix: include f_to in test_filters subTest labels so each of the 8 cases reports distinctly

* Fix: run the post_consume error-log assertion after the raising call and match the actual paperless_mail logger name

* Fix: rename the blank-password workflow test to match its behavior and add a real wrong-password-fails test

* Fix: use the created social account's actual pk and remove an accidental tuple wrapping the mock provider

* Fix: assert against the created documents' actual pks instead of hardcoded 1 and 2

* Fix: assert test_compression actually produces a valid LZMA-compressed zip

* Fix: clear os.environ when patching PAPERLESS_ADMIN_* vars so a host-set value can't leak into the no-user test

* Fix: restore MIDDLEWARE, AUTHENTICATION_BACKENDS and REST_FRAMEWORK auth classes after each remote-user settings test instead of leaking the mutation into later tests

* Fix: use a guaranteed-nonexistent temp path instead of hardcoded /tmp/foo/bar in test_export_target_not_exists
2026-09-23 19:11:58 +00:00
GitHub Actions 969c2ea0e2 Auto translate strings 2026-09-23 19:00:47 +00:00
shamoon 31b806a285 Fix: ensure bulk operations are checked against version root (#14246) 2026-09-23 18:59:27 +00:00
GitHub Actions 99851b418c Auto translate strings 2026-09-23 18:46:43 +00:00
e34eda07bb Enhancement: support separate embedding API key (#14067)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-09-23 11:44:37 -07:00
GitHub Actions 793459b416 Auto translate strings 2026-09-23 18:22:05 +00:00
shamoon 04297fd02c Enhancement: support passthrough extra params for LLMs (#14202) 2026-09-23 18:20:43 +00:00
dependabot[bot] 1b277dd8e1 Chore(deps): Bump autobahn in the uv group across 1 directory (#14231)
Bumps the uv group with 1 update in the / directory: [autobahn](https://github.com/crossbario/autobahn-python).


Updates `autobahn` from 25.12.2 to 26.7.1
- [Release notes](https://github.com/crossbario/autobahn-python/releases)
- [Changelog](https://github.com/crossbario/autobahn-python/blob/master/docs/changelog.rst)
- [Commits](https://github.com/crossbario/autobahn-python/compare/v25.12.2...v26.7.1)

---
updated-dependencies:
- dependency-name: autobahn
  dependency-version: 26.7.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 15:33:01 +00:00
shamoon 3c20abeb4c Fix: indexing after document-added workflows signal (#14242) 2026-09-23 14:50:48 +00:00
GitHub Actions b11f1f8459 Auto translate strings 2026-09-23 03:19:06 +00:00
Trenton H 7424e7ce0b Fix: Record full tag and custom field lists in bulk edit audit log (#14236)
Snapshot only the edited field, before and after the operation, gathering tags
and custom field instances into sorted id lists per document (empty when there
are none).
2026-09-23 03:17:40 +00:00
shamoon a53a3d3769 Chore: update pikepdf for ocrmypdf requirement (#14235) 2026-09-22 19:01:21 -07:00
Trenton H 15b73b890c Chore: Ban cross-app imports of documents.tests (#14224)
Nothing outside the documents app imports documents.tests any more.

Ruff now bans the module everywhere except src/documents/tests, where in-app imports remain fine.
2026-09-22 08:02:18 -07:00
Trenton H 02d355061f Chore: Move helpers that other test modules imported out of test files (#14223)
The mail message and mailbox builders, the fake libmagic and the classifier preprocessor stub lived inside test_mail.py and test_classifier.py, so other test modules imported them by importing a test module.

They now live in helpers modules beside the tests that use them.
2026-09-22 08:02:18 -07:00
Trenton H d8b5b4d447 Chore: Stop the live-service retry helper sleeping after a success (#14222)
util_call_with_backoff made every call wait 20 seconds even when the first attempt succeeded

The helper now sleeps only after a failure that has another attempt left.
2026-09-22 08:02:18 -07:00
Trenton H 03ac4aed7e Chore: Move the cross-app test helpers into the shared layer and add a progress fixture (#14221)
Test modules in paperless, paperless_mail and documents imported filesystem assertions, the migration test base, the retry helper and the streaming-response reader out of documents/tests/utils.py, which kept each app's tests coupled to another app's test package.

They now live in paperless_testing, and the progress manager fake is renamed FakeProgressManager and now subclasses the real ProgressManager, overriding only the transport, so the payload it records is built by the production code. The twenty places that patched documents.tasks.ProgressManager by hand now use a fake_progress_manager fixture.
2026-09-22 08:02:17 -07:00
Trenton H 90d23bad9c Chore: Give the search index directory one owner in the tests (#14217)
Two fixtures created a temporary index directory and pointed INDEX_DIR at it, and paperless_dirs did the same, so a test that requested more than one got whichever assignment ran last. The search conftest no longer defines its own index_dir fixture, the tests that took it read paperless_dirs.index_dir instead, and _search_index is now a thin wrapper that requests paperless_dirs. The fixture that yields a Document is renamed from indexed_document to searchable_document so it no longer differs by one character from the index_document factory next to it.
2026-09-22 08:02:17 -07:00
Trenton H e4367b5648 Chore: Delete the in-memory Tantivy backend and run the search tests on disk (#14216)
TantivyBackend(path=None) built an in-memory index that is not used by production ever used. The backend now requires a path, the open, write-batch and rebuild branches are gone, and the shared backend fixture and the fulltext similar-documents fixture build a real index under the per-test directory layout.
2026-09-22 08:02:16 -07:00
Trenton H cb85441c2f Chore: Decrease backend test suite time (a little) (#14213)
* Chore: Speed up test setup by hashing passwords with MD5 and batching index writes

Don't use Django's default PBKDF2, about 600 ms per use and 110 uses across the suite. Switches to MD5 instead.

Also fixes a test that didn't batch update the search index

* Chore: Stop the invalid webhook params test from waiting on a Celery broker

test_workflow_webhook_action_url_invalid_params_headers left send_webhook.apply_async unpatched, so it tried to actually enqueu and waited for the timeout.
2026-09-21 20:17:40 +00:00
Trenton H cceaa559d4 Chore: Rename sample directory fixtures and drop unused parser ones (#14215)
Nineteen single-file fixtures in the parsers conftest had no consumers anywhere in the test tree.

Two fixtures were both called samples_dir and resolved one directory apart They are now document_samples_dir and parser_samples_dir
2026-09-21 12:59:32 -07:00
GitHub Actions a748d4c64f Auto translate strings 2026-09-21 19:01:49 +00:00
shamoon 452ed005bd Fix: handle legacy bulk edit page range with missing page_count (#14212) 2026-09-21 19:00:14 +00:00
Trenton H 40058ff7d5 Chore: Give the test suite a larger regex timeout (#14211)
Maybe the random ordering sometimes causes heavier tests to run alongside timed regex ones?
2026-09-21 11:38:54 -07:00
Trenton H f502cd5e34 Chore: Add shared helpers for granting test permissions and use them (#14200) 2026-09-21 10:57:14 -07:00
Trenton H 99ce6b5db3 Chore: Share the API client fixtures and build test users cheaply (#14199) 2026-09-21 10:57:14 -07:00
Trenton H 2d955e9697 Chore: Randomize test order and seed Faker per run (#14173)
Enables pytest-randomly, which has sat commented out in pyproject.toml
since the Pytest 9 upgrade. Tests now run in a different order every
session, so a test cannot quietly depend on another having run first.
2026-09-21 10:57:13 -07:00
Trenton H f440e8d33c Chore: Move unittest directory setup onto the shared fixture (#14172)
The unittest side of the suite built its temp directory tree with
tempfile.mkdtemp and a manually enabled override_settings, cleaned up only if
tearDown ran. That is now gone. DirectoriesMixin lives alongside the layout it
bridges and does nothing but hand the paperless_dirs fixture to TestCase
subclasses as self.dirs, so both halves of the suite get the same twelve
settings, the same directory shapes and cleanup owned by tmp_path.

The mixin moves to paperless_testing.dirs rather than staying in the documents
test utilities, because modules in paperless and paperless_mail import it
across the app boundary. The thirty-eight consuming modules change only their
import line; self.dirs.scratch_dir and its siblings keep working.
2026-09-21 10:57:12 -07:00
Trenton H 4c264651e8 Chore: State the test directory layout in one place (#14171)
The temp directory layout used by the tests was written out four separate
times: once in the documents conftest, once in the paperless checks tests,
once in a fixture local to the NFC upload tests, and once in the helper
behind the old paperless_environment context manager. Each copy covered a
different subset of the settings, so which directories a test actually got
depended on which copy it happened to reach.

The layout now lives in paperless_testing.dirs. build_paperless_dirs owns
where things go and creates them, dirs_settings maps them onto Django
setting names and is pure, and a paperless_dirs fixture in the root conftest
applies that mapping through pytest-django's settings fixture so every app
can reach it. Tests that need a second environment part way through a test
body use the paperless_environment context manager from the same module,
which expresses the identical layout through override_settings. The three
redundant implementations and the old media settings fixture are gone, and
their consumers now take paperless_dirs.
2026-09-21 10:57:11 -07:00
Trenton H 48d97b78bb Chore: Move model factories to the shared test layer (#14170)
The model factories lived in the documents test package, but three other
apps needed them. The AI, mail and testing suites all reached across an app
boundary to import from documents.tests.factories, which made a private test
package into a shared dependency.

The factories now live in the shared testing package, where cross-app use is
the intended use.
2026-09-21 10:57:11 -07:00
Trenton H 659a0cb2ef Chore: Add a shared test support layer (#14168)
All four Django apps have test code in common, but the only place to put it
was the documents app's own tests package, so paperless, paperless_ai and
paperless_mail each reached across an app boundary to import helpers and
relied on fixtures that were only defined for the documents test path.

This adds a root src/conftest.py holding the fixtures every app needs and an
new src/paperless_testing package for shared helpers a test names
2026-09-21 10:57:11 -07:00
Trenton H d02d1e1711 Chore: Remove unused test helpers (#14166) 2026-09-21 10:57:10 -07:00
dependabot[bot]andstumpylog 3b41810e7b Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 2 updates (#14133)
* Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 2 updates

Bumps the pre-commit-dependencies group with 2 updates in the / directory: [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit) and [https://github.com/tox-dev/pyproject-fmt](https://github.com/tox-dev/pyproject-fmt).


Updates `https://github.com/astral-sh/ruff-pre-commit` from v0.16.5 to 0.16.7
- [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases)
- [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.5...v0.16.7)

Updates `https://github.com/tox-dev/pyproject-fmt` from v2.28.1 to 2.29.4
- [Release notes](https://github.com/tox-dev/pyproject-fmt/releases)
- [Commits](https://github.com/tox-dev/pyproject-fmt/compare/v2.28.1...v2.29.4)

---
updated-dependencies:
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.16.6
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/tox-dev/pyproject-fmt
  dependency-version: 2.29.4
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Runs new formatting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stumpylog <797416+stumpylog@users.noreply.github.com>
2026-09-21 07:49:12 -07:00
Zhiyuan Zhengandzhzy0077 12314fcaa8 Fix: ignore invalid EXIF orientation when generating image archives (#14203)
Images with an out-of-spec EXIF orientation value (e.g. 0) fail
conversion with img2pdf.ExifOrientationError, which aborts archive
generation and, during consumption with OCR disabled, fails the whole
document.

Pass rotation=img2pdf.Rotation.ifvalid so invalid orientation values
are ignored while valid values (1, 3, 6, 8) are still applied.

Co-authored-by: zhzy0077 <zhzy0077@users.noreply.github.com>
2026-09-21 07:21:24 -07:00
github-actions[bot] 8705bd510a Changelog v3.2.1 - GHA (#14198)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-20 15:00:56 -07:00
Trenton H 7575d60782 Bump version to 3.2.1 2026-09-20 14:29:37 -07:00
github-actions[bot]andCrowdin Bot 8b02a23b86 New Crowdin translations by GitHub Action (#14179)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-20 21:24:50 +00:00
dependabot[bot] 87d157096e Chore(deps): Bump anyio in the uv group across 1 directory (#14175)
Bumps the uv group with 1 update in the / directory: [anyio](https://github.com/agronholm/anyio).


Updates `anyio` from 4.12.1 to 4.14.2
- [Release notes](https://github.com/agronholm/anyio/releases)
- [Commits](https://github.com/agronholm/anyio/compare/4.12.1...4.14.2)

---
updated-dependencies:
- dependency-name: anyio
  dependency-version: 4.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-20 13:21:27 +00:00
Bitfoo 54e332d259 Fix: only pass --conf to flower when flowerconfig.py exists (#14182)
* Fix: only pass --conf to flower when flowerconfig.py exists

The service passes --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py
unconditionally, but the image does not ship that file. flower 2.0.1 tolerated
this because it ignored a missing config whose basename matched its own default
name; 2.1.0 dropped that basename() call (mher/flower#1391) and now raises
FileNotFoundError, so flower crash-loops and never binds 5555.

Pass the flag only when the file is present.
2026-09-19 21:10:52 -07:00
Trenton H 506afb4200 Fix: replace stale mail-fetch overlap check with a self-expiring lock (#14189) 2026-09-19 20:02:10 -07:00
Trenton H 22a8a4f4ad Fix: bump ocrmypdf to 17.12 to pick up the ligature text-layer fix (#14190) 2026-09-19 15:24:56 -07:00
Trenton H d5425a893d Fix: rebuild the search index automatically when it is corrupted, instead of hard-failing (#14180) 2026-09-19 12:53:36 -07:00
github-actions[bot]andshamoon cc25144dbf Documentation: Add v3.2.0 changelog (#14176)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-09-18 20:15:48 -07:00
281 changed files with 31425 additions and 34451 deletions
+2
View File
@@ -15,6 +15,8 @@
# Test related # Test related
**/.pytest_cache **/.pytest_cache
**/tests **/tests
src/paperless_testing
src/conftest.py
**/*.spec.ts **/*.spec.ts
**/htmlcov **/htmlcov
# Local folders # Local folders
-1
View File
@@ -59,7 +59,6 @@ updates:
- "drf-*" - "drf-*"
- "djangorestframework" - "djangorestframework"
- "whitenoise" - "whitenoise"
- "bleach"
- "jinja2" - "jinja2"
# Async, Task Queuing & Caching # Async, Task Queuing & Caching
async-tasks: async-tasks:
+4 -4
View File
@@ -102,7 +102,7 @@ jobs:
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
@@ -139,13 +139,13 @@ jobs:
pytest pytest
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: always() if: always()
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: junit.xml files: junit.xml
report_type: test_results report_type: test_results
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: coverage.xml files: coverage.xml
@@ -176,7 +176,7 @@ jobs:
with: with:
python-version: "${{ env.DEFAULT_PYTHON }}" python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+3 -3
View File
@@ -106,7 +106,7 @@ jobs:
echo "repository=${repo_name}" echo "repository=${repo_name}"
echo "name=${repo_name}" >> $GITHUB_OUTPUT echo "name=${repo_name}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
@@ -132,7 +132,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -182,7 +182,7 @@ jobs:
echo "Downloaded digests:" echo "Downloaded digests:"
ls -la /tmp/digests/ ls -la /tmp/digests/
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+3 -3
View File
@@ -174,13 +174,13 @@ jobs:
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: always() if: always()
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/ directory: src-ui/
report_type: test_results report_type: test_results
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/ directory: src-ui/coverage/
@@ -216,7 +216,7 @@ jobs:
with: with:
python-version: '3.12' python-version: '3.12'
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: '0.12.x' version: '0.12.x'
enable-cache: false enable-cache: false
+2 -2
View File
@@ -59,7 +59,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
@@ -212,7 +212,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
- name: Run Semgrep - name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning - name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
if: always() if: always()
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@4fe057d991d63b8f6d5d22c40f17c1bca2226537 # v0.12.0 uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
@@ -56,7 +56,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@4fe057d991d63b8f6d5d22c40f17c1bca2226537 # v0.12.0 uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,4 +47,4 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.PNGX_BOT_PAT }} token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false persist-credentials: false
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3.0.2 uses: crowdin/github-action@df474cdfb9f41d6ae777118749477c2cdf7cacc8 # v3.2.0
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
sudo apt-get update -qq sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+706 -456
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -38,7 +38,7 @@ repos:
- json - json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason # See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.9.6' rev: 'v3.9.8'
hooks: hooks:
- id: prettier - id: prettier
types_or: types_or:
@@ -46,16 +46,16 @@ repos:
- ts - ts
- markdown - markdown
additional_dependencies: additional_dependencies:
- prettier@3.9.6 - prettier@3.9.8
- 'prettier-plugin-organize-imports@4.3.0' - 'prettier-plugin-organize-imports@4.3.0'
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.5 rev: v0.16.8
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.28.1" rev: "v2.29.4"
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
additional_dependencies: [tomli] additional_dependencies: [tomli]
+4804 -12941
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.12.16-python3.14-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.12.20-python3.14-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
@@ -171,7 +171,9 @@ RUN set -eux \
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \ && cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& echo "Cleaning up image layer" \ && echo "Cleaning up image layer" \
&& rm --force --verbose *.deb \ && rm --force --verbose *.deb \
&& rm --recursive --force --verbose /var/lib/apt/lists/* && rm --recursive --force --verbose /var/lib/apt/lists/* \
&& echo "Configuring interactive shells to source the s6 container environment" \
&& echo '. /etc/profile.d/contenv.sh' >> /etc/bash.bashrc
WORKDIR /usr/src/paperless/src/ WORKDIR /usr/src/paperless/src/
+1 -1
View File
@@ -24,7 +24,7 @@ services:
network_mode: host network_mode: host
restart: unless-stopped restart: unless-stopped
greenmail: greenmail:
image: docker.io/greenmail/standalone:2.1.13 image: docker.io/greenmail/standalone:2.1.14
hostname: greenmail hostname: greenmail
container_name: greenmail container_name: greenmail
environment: environment:
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# Source s6 container environment for interactive shells.
# Ensures variables resolved from *_FILE secret injection are visible
# when using 'docker exec bash'. Does not affect s6 services (those
# use with-contenv directly). Has no effect in non-container contexts
# because the directory will not exist.
# Note: sh/dash shells opened via 'docker exec sh' are not covered;
# only bash-based sessions benefit from this file.
_pngx_contenv="/run/s6/container_environment"
if [ -d "${_pngx_contenv}" ]; then
for _pngx_f in "${_pngx_contenv}"/*; do
[ -f "${_pngx_f}" ] || continue
_pngx_name=$(basename "${_pngx_f}")
_pngx_val=$(cat "${_pngx_f}")
export "${_pngx_name}=${_pngx_val}"
done
fi
unset _pngx_contenv _pngx_f _pngx_name _pngx_val
@@ -2,6 +2,7 @@
# shellcheck shell=bash # shellcheck shell=bash
declare -r log_prefix="[svc-flower]" declare -r log_prefix="[svc-flower]"
declare -r flower_config="${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py"
echo "${log_prefix} Checking if we should start flower..." echo "${log_prefix} Checking if we should start flower..."
@@ -9,12 +10,20 @@ if [[ -n "${PAPERLESS_ENABLE_FLOWER}" ]]; then
# Small delay to allow celery to be up first # Small delay to allow celery to be up first
echo "${log_prefix} Starting flower in 5s" echo "${log_prefix} Starting flower in 5s"
sleep 5 sleep 5
cd ${PAPERLESS_SRC_DIR} cd "${PAPERLESS_SRC_DIR}" || exit 1
# Only pass --conf if the file is actually there. The image does not ship one, and
# flower >= 2.1.0 exits with FileNotFoundError when an explicitly given --conf path
# does not exist (mher/flower#1391). Earlier versions silently ignored it.
declare -a conf_args=()
if [[ -f "${flower_config}" ]]; then
conf_args=(--conf="${flower_config}")
fi
if [[ -n "${USER_IS_NON_ROOT}" ]]; then if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec /usr/local/bin/celery --app paperless flower --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py exec /usr/local/bin/celery --app paperless flower "${conf_args[@]}"
else else
exec s6-setuidgid paperless /usr/local/bin/celery --app paperless flower --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py exec s6-setuidgid paperless /usr/local/bin/celery --app paperless flower "${conf_args[@]}"
fi fi
else else
+14 -9
View File
@@ -136,13 +136,15 @@ for suggested generation and embedding models.
### AI-assisted suggestions ### AI-assisted suggestions
With AI enabled, Paperless-ngx can suggest a title, tags, correspondent, document type, With AI enabled, Paperless-ngx can suggest a title, tags, correspondent, document type,
storage path and dates by sending the document to the LLM. This is **opt-in per request** storage path and dates by sending the document to the LLM using "Suggest" button on the document
and surfaces through the "Suggest" control on the document detail page, alongside the detail page. You can choose which type of suggestions are requested by default under Settings >
classic classifier-based suggestions — it does not disable them. Suggestions are requested Documents, either ML (classifier-based) suggestions, AI suggestions, or both. When both are requested
automatically when you open a document that carries an inbox tag unless "Automatically request the results are combined.
suggestions for inbox documents" under Settings > Documents is disabled. Suggestion output
language can be steered with Suggestions are requested automatically when you open a document that carries an inbox tag
[`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE`](configuration.md#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE) unless "Automatically request suggestions for inbox documents" under Settings > Documents is disabled.
Suggestion output language can be steered with [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE`](configuration.md#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE)
(otherwise it follows the user's UI language). (otherwise it follows the user's UI language).
### The LLM index (RAG) and similar documents ### The LLM index (RAG) and similar documents
@@ -153,8 +155,11 @@ in similar existing documents, and the document chat can retrieve relevant conte
Enable it by setting Enable it by setting
[`PAPERLESS_AI_LLM_EMBEDDING_BACKEND`](configuration.md#PAPERLESS_AI_LLM_EMBEDDING_BACKEND) [`PAPERLESS_AI_LLM_EMBEDDING_BACKEND`](configuration.md#PAPERLESS_AI_LLM_EMBEDDING_BACKEND)
(`huggingface` for fully-local embeddings, or `ollama` / `openai-like`). The index is only (`huggingface` for fully-local embeddings, or `ollama` / `openai-like`). By default, the main
built when AI is enabled **and** an embedding backend is set. LLM API key and endpoint are used, but an optional embedding-specific[API key](configuration.md#PAPERLESS_AI_LLM_EMBEDDING_API_KEY)
and [endpoint](configuration.md#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT) can be configured.
The index is only built when AI is enabled **and** an embedding backend is set.
The index is updated automatically on a schedule controlled by The index is updated automatically on a schedule controlled by
[`PAPERLESS_LLM_INDEX_TASK_CRON`](configuration.md#PAPERLESS_LLM_INDEX_TASK_CRON) (daily by [`PAPERLESS_LLM_INDEX_TASK_CRON`](configuration.md#PAPERLESS_LLM_INDEX_TASK_CRON) (daily by
+205
View File
@@ -1,5 +1,210 @@
# Changelog # Changelog
## paperless-ngx 3.2.1
### Bug Fixes
- Fix: only pass --conf to flower when flowerconfig.py exists [@bitfoo1](https://github.com/bitfoo1) ([#14182](https://github.com/paperless-ngx/paperless-ngx/pull/14182))
- Fix: replace stale mail-fetch overlap check with a self-expiring lock [@stumpylog](https://github.com/stumpylog) ([#14189](https://github.com/paperless-ngx/paperless-ngx/pull/14189))
- Fix: bump ocrmypdf to 17.12 to pick up the ligature text-layer fix [@stumpylog](https://github.com/stumpylog) ([#14190](https://github.com/paperless-ngx/paperless-ngx/pull/14190))
- Fix: rebuild the search index automatically when it is missing Tantivy files [@stumpylog](https://github.com/stumpylog) ([#14180](https://github.com/paperless-ngx/paperless-ngx/pull/14180))
### Dependencies
- Chore(deps): Bump anyio from 4.12.1 to 4.14.2 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14175](https://github.com/paperless-ngx/paperless-ngx/pull/14175))
### All App Changes
<details>
<summary>4 changes</summary>
- Chore(deps): Bump anyio from 4.12.1 to 4.14.2 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14175](https://github.com/paperless-ngx/paperless-ngx/pull/14175))
- Fix: replace stale mail-fetch overlap check with a self-expiring lock [@stumpylog](https://github.com/stumpylog) ([#14189](https://github.com/paperless-ngx/paperless-ngx/pull/14189))
- Fix: bump ocrmypdf to 17.12 to pick up the ligature text-layer fix [@stumpylog](https://github.com/stumpylog) ([#14190](https://github.com/paperless-ngx/paperless-ngx/pull/14190))
- Fix: rebuild the search index automatically when it is missing Tantivy files [@stumpylog](https://github.com/stumpylog) ([#14180](https://github.com/paperless-ngx/paperless-ngx/pull/14180))
</details>
## paperless-ngx 3.2.0
### Features / Enhancements
- Enhancement (QoL): support deselecting single items from "select all" [@shamoon](https://github.com/shamoon) ([#14117](https://github.com/paperless-ngx/paperless-ngx/pull/14117))
- Enhancement: Match fuzzy terms in place inside the parsed query [@stumpylog](https://github.com/stumpylog) ([#14157](https://github.com/paperless-ngx/paperless-ngx/pull/14157))
- Enhancement: Match CJK terms through their bigram fields in place [@stumpylog](https://github.com/stumpylog) ([#14156](https://github.com/paperless-ngx/paperless-ngx/pull/14156))
- Enhancement: centralized management of share links + bundles [@shamoon](https://github.com/shamoon) ([#14115](https://github.com/paperless-ngx/paperless-ngx/pull/14115))
- Enhancement: parse advanced search with whoosh-compat and delete the handwritten translation [@stumpylog](https://github.com/stumpylog) ([#14072](https://github.com/paperless-ngx/paperless-ngx/pull/14072))
- Enhancement: allow regex timeout configuration [@shamoon](https://github.com/shamoon) ([#14085](https://github.com/paperless-ngx/paperless-ngx/pull/14085))
- Enhancement: hide-able sidebar items [@shamoon](https://github.com/shamoon) ([#14052](https://github.com/paperless-ngx/paperless-ngx/pull/14052))
- Enhancement: Improve matching for correspondents, storage path and labels by removing bias + adding minimum match threshold [@dewey](https://github.com/dewey) ([#12164](https://github.com/paperless-ngx/paperless-ngx/pull/12164))
- Enhancement: add Tantivy full-text fallback adapter for taxonomy candidates [@stumpylog](https://github.com/stumpylog) ([#13820](https://github.com/paperless-ngx/paperless-ngx/pull/13820))
- Enhancement (QoL): surface externally-set options in Config UI [@shamoon](https://github.com/shamoon) ([#13989](https://github.com/paperless-ngx/paperless-ngx/pull/13989))
- Enhancement: allow disabling auto-suggestions for inbox documents [@shamoon](https://github.com/shamoon) ([#13946](https://github.com/paperless-ngx/paperless-ngx/pull/13946))
- Change: skip documents with empty content in apply AI suggestions WF [@shamoon](https://github.com/shamoon) ([#13985](https://github.com/paperless-ngx/paperless-ngx/pull/13985))
- Enhancement: duplicates filter [@shamoon](https://github.com/shamoon) ([#13994](https://github.com/paperless-ngx/paperless-ngx/pull/13994))
- Tweak: note that apply AI suggestions runs async in WF editor [@shamoon](https://github.com/shamoon) ([#14004](https://github.com/paperless-ngx/paperless-ngx/pull/14004))
- Enhancement (QoL): attempt to localize firstDayOfWeek for date picker [@shamoon](https://github.com/shamoon) ([#13999](https://github.com/paperless-ngx/paperless-ngx/pull/13999))
### Bug Fixes
- Fix: don't redirect to signup on first install when regular login is disabled [@cyberb](https://github.com/cyberb) ([#14165](https://github.com/paperless-ngx/paperless-ngx/pull/14165))
- Fix: better catch email workflow placeholder parsing errors [@shamoon](https://github.com/shamoon) ([#14129](https://github.com/paperless-ngx/paperless-ngx/pull/14129))
- Fix: validate legacy bulk edit owner, rotate and split parameters [@stumpylog](https://github.com/stumpylog) ([#14120](https://github.com/paperless-ngx/paperless-ngx/pull/14120))
- Fix: validate set\_permissions with a nested serializer [@stumpylog](https://github.com/stumpylog) ([#14119](https://github.com/paperless-ngx/paperless-ngx/pull/14119))
- Fix: Use prefetching to reduce query counts during classifier training [@stumpylog](https://github.com/stumpylog) ([#14122](https://github.com/paperless-ngx/paperless-ngx/pull/14122))
- Fix: reject non-dict user\_args/barcode\_tag\_mapping in config API [@stumpylog](https://github.com/stumpylog) ([#14118](https://github.com/paperless-ngx/paperless-ngx/pull/14118))
- Fix: type edit\_pdf operations via a nested serializer [@stumpylog](https://github.com/stumpylog) ([#14116](https://github.com/paperless-ngx/paperless-ngx/pull/14116))
- Fix: ensure django setup is run for management comments under 3.14 [@shamoon](https://github.com/shamoon) ([#14100](https://github.com/paperless-ngx/paperless-ngx/pull/14100))
- Fix: validate PDF output doc indexes in bulk edit [@shamoon](https://github.com/shamoon) ([#14083](https://github.com/paperless-ngx/paperless-ngx/pull/14083))
- Fix: avoid IntegrityError when a retried task republishes with the same ID [@stumpylog](https://github.com/stumpylog) ([#14096](https://github.com/paperless-ngx/paperless-ngx/pull/14096))
- Fix: update some api global perms inconsistencies [@shamoon](https://github.com/shamoon) ([#14086](https://github.com/paperless-ngx/paperless-ngx/pull/14086))
- Fix: ignore nested action IDs on WF create [@shamoon](https://github.com/shamoon) ([#14084](https://github.com/paperless-ngx/paperless-ngx/pull/14084))
- Fix: correct text/stream compression workaround [@shamoon](https://github.com/shamoon) ([#14064](https://github.com/paperless-ngx/paperless-ngx/pull/14064))
- Fix: ui version content switching inconsistencies [@shamoon](https://github.com/shamoon) ([#14066](https://github.com/paperless-ngx/paperless-ngx/pull/14066))
- Fix: prevent saving changes to stale cached document object [@shamoon](https://github.com/shamoon) ([#14065](https://github.com/paperless-ngx/paperless-ngx/pull/14065))
- Fix: ensure remove inbox tag children on remove\_inbox\_tags [@shamoon](https://github.com/shamoon) ([#14050](https://github.com/paperless-ngx/paperless-ngx/pull/14050))
- Fix: connect add\_to\_index handler after document\_added [@shamoon](https://github.com/shamoon) ([#14058](https://github.com/paperless-ngx/paperless-ngx/pull/14058))
- Fix: Drop empty files from tracking after the stability window has passed [@stumpylog](https://github.com/stumpylog) ([#14047](https://github.com/paperless-ngx/paperless-ngx/pull/14047))
- Fixhancement: better LLM errors [@shamoon](https://github.com/shamoon) ([#14031](https://github.com/paperless-ngx/paperless-ngx/pull/14031))
- Fix: prevent orphaned versions from bulk delete [@shamoon](https://github.com/shamoon) ([#14030](https://github.com/paperless-ngx/paperless-ngx/pull/14030))
- Fixhancement: prevent overlapping mail-account processing runs [@stumpylog](https://github.com/stumpylog) ([#14046](https://github.com/paperless-ngx/paperless-ngx/pull/14046))
- Fix: Use PAPERLESS\_REDIS\_PREFIX for Celery result backend keys [@bdd](https://github.com/bdd) ([#14015](https://github.com/paperless-ngx/paperless-ngx/pull/14015))
- Fix: correct setting ai\_enabled to false via UI [@shamoon](https://github.com/shamoon) ([#13987](https://github.com/paperless-ngx/paperless-ngx/pull/13987))
- Fix: catch some frontend failed object retrievals [@shamoon](https://github.com/shamoon) ([#14023](https://github.com/paperless-ngx/paperless-ngx/pull/14023))
- Fix: more v3 icons cleanup [@shamoon](https://github.com/shamoon) ([#14017](https://github.com/paperless-ngx/paperless-ngx/pull/14017))
- Fix: correct add version actor parity [@shamoon](https://github.com/shamoon) ([#14016](https://github.com/paperless-ngx/paperless-ngx/pull/14016))
- Fix: fix v3 favicon file [@shamoon](https://github.com/shamoon) ([#14014](https://github.com/paperless-ngx/paperless-ngx/pull/14014))
- Fix: change share link bundle dialog button to close after create, don't toast on copied [@shamoon](https://github.com/shamoon) ([#14002](https://github.com/paperless-ngx/paperless-ngx/pull/14002))
- Fix: truncate mail subjects to field max length [@shamoon](https://github.com/shamoon) ([#13991](https://github.com/paperless-ngx/paperless-ngx/pull/13991))
- Fix: enforce the overflow hidden rule on pdf editor thumbnails [@shamoon](https://github.com/shamoon) ([#13976](https://github.com/paperless-ngx/paperless-ngx/pull/13976))
- Fix: also correct unbroken long names on small cards [@shamoon](https://github.com/shamoon) ([#13974](https://github.com/paperless-ngx/paperless-ngx/pull/13974))
- Fix: ensure parent + child tags change together in bulk editor [@shamoon](https://github.com/shamoon) ([#13972](https://github.com/paperless-ngx/paperless-ngx/pull/13972))
### Dependencies
<details>
<summary>29 changes</summary>
- Chore(deps): Bump the utilities-patch group across 1 directory with 15 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14167](https://github.com/paperless-ngx/paperless-ngx/pull/14167))
- docker(deps): Bump astral-sh/uv from 0.12.9-python3.14-trixie-slim to 0.12.16-python3.14-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#14134](https://github.com/paperless-ngx/paperless-ngx/pull/14134))
- Chore(deps): Bump the utilities-minor group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14149](https://github.com/paperless-ngx/paperless-ngx/pull/14149))
- Chore(deps): Bump the actions group across 1 directory with 8 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14162](https://github.com/paperless-ngx/paperless-ngx/pull/14162))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 19 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14141](https://github.com/paperless-ngx/paperless-ngx/pull/14141))
- docker-compose(deps): bump gotenberg/gotenberg from 8.36 to 8.37 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14137](https://github.com/paperless-ngx/paperless-ngx/pull/14137))
- docker-compose(deps): Bump nginx from 1.31.5-alpine to 1.31.6-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14138](https://github.com/paperless-ngx/paperless-ngx/pull/14138))
- Chore(deps): Bump pdfjs-dist from 6.2.108 to 6.3.289 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14144](https://github.com/paperless-ngx/paperless-ngx/pull/14144))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14143](https://github.com/paperless-ngx/paperless-ngx/pull/14143))
- Chore(deps-dev): Bump @types/node from 26.4.0 to 26.5.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14146](https://github.com/paperless-ngx/paperless-ngx/pull/14146))
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14142](https://github.com/paperless-ngx/paperless-ngx/pull/14142))
- Chore(deps): Bump the utilities-minor group across 1 directory with 11 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13988](https://github.com/paperless-ngx/paperless-ngx/pull/13988))
- Chore(deps): Bump sentence-transformers from 5.6.1 to 6.0.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#13983](https://github.com/paperless-ngx/paperless-ngx/pull/13983))
- Chore(deps-dev): Bump types-markdown from 3.10.2.20260518 to 3.10.2.20260712 @[dependabot[bot]](https://github.com/apps/dependabot) ([#13982](https://github.com/paperless-ngx/paperless-ngx/pull/13982))
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13980](https://github.com/paperless-ngx/paperless-ngx/pull/13980))
- Chore(deps): Update granian[uvloop] requirement from ~=2.7.0 to >=2.7,\<2.9 @[dependabot[bot]](https://github.com/apps/dependabot) ([#13984](https://github.com/paperless-ngx/paperless-ngx/pull/13984))
- Chore: Updates our direct Redis pin [@stumpylog](https://github.com/stumpylog) ([#13986](https://github.com/paperless-ngx/paperless-ngx/pull/13986))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13918](https://github.com/paperless-ngx/paperless-ngx/pull/13918))
- Chore(deps): Bump uuid from 14.0.1 to 14.0.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13921](https://github.com/paperless-ngx/paperless-ngx/pull/13921))
- Chore(deps-dev): Bump @types/node from 26.2.0 to 26.4.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13919](https://github.com/paperless-ngx/paperless-ngx/pull/13919))
- Chore: update ng-select to v24, handle breaking changes [@shamoon](https://github.com/shamoon) ([#13951](https://github.com/paperless-ngx/paperless-ngx/pull/13951))
- Chore(deps): Bump djangorestframework from 3.17.2 to 3.18.0 in the django-ecosystem group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13912](https://github.com/paperless-ngx/paperless-ngx/pull/13912))
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13922](https://github.com/paperless-ngx/paperless-ngx/pull/13922))
- Chore(deps): Bump the document-processing group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13916](https://github.com/paperless-ngx/paperless-ngx/pull/13916))
- Chore(deps): Bump flower from 2.0.1 to 2.1.0 in the async-tasks group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13913](https://github.com/paperless-ngx/paperless-ngx/pull/13913))
- Chore(deps): Bump the actions group across 1 directory with 15 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13920](https://github.com/paperless-ngx/paperless-ngx/pull/13920))
- docker-compose(deps): Bump gotenberg/gotenberg from 8.34 to 8.36 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#13910](https://github.com/paperless-ngx/paperless-ngx/pull/13910))
- Chore(deps-dev): Bump the development group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13911](https://github.com/paperless-ngx/paperless-ngx/pull/13911))
- docker(deps): Bump astral-sh/uv from 0.12.5-python3.14-trixie-slim to 0.12.9-python3.14-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#13914](https://github.com/paperless-ngx/paperless-ngx/pull/13914))
</details>
### All App Changes
<details>
<summary>80 changes</summary>
- Chore(deps): Bump the utilities-patch group across 1 directory with 15 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14167](https://github.com/paperless-ngx/paperless-ngx/pull/14167))
- Fix: don't redirect to signup on first install when regular login is disabled [@cyberb](https://github.com/cyberb) ([#14165](https://github.com/paperless-ngx/paperless-ngx/pull/14165))
- Chore(deps): Bump the utilities-minor group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14149](https://github.com/paperless-ngx/paperless-ngx/pull/14149))
- Enhancement (QoL): support deselecting single items from "select all" [@shamoon](https://github.com/shamoon) ([#14117](https://github.com/paperless-ngx/paperless-ngx/pull/14117))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 19 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14141](https://github.com/paperless-ngx/paperless-ngx/pull/14141))
- Enhancement: Match fuzzy terms in place inside the parsed query [@stumpylog](https://github.com/stumpylog) ([#14157](https://github.com/paperless-ngx/paperless-ngx/pull/14157))
- Enhancement: Match CJK terms through their bigram fields in place [@stumpylog](https://github.com/stumpylog) ([#14156](https://github.com/paperless-ngx/paperless-ngx/pull/14156))
- Chore(deps): Bump pdfjs-dist from 6.2.108 to 6.3.289 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14144](https://github.com/paperless-ngx/paperless-ngx/pull/14144))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14143](https://github.com/paperless-ngx/paperless-ngx/pull/14143))
- Chore(deps-dev): Bump @types/node from 26.4.0 to 26.5.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14146](https://github.com/paperless-ngx/paperless-ngx/pull/14146))
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14142](https://github.com/paperless-ngx/paperless-ngx/pull/14142))
- Enhancement: centralized management of share links + bundles [@shamoon](https://github.com/shamoon) ([#14115](https://github.com/paperless-ngx/paperless-ngx/pull/14115))
- Performance: Preprocess classifier text with Tantivy instead of NLTK [@stumpylog](https://github.com/stumpylog) ([#14127](https://github.com/paperless-ngx/paperless-ngx/pull/14127))
- Performance: Drops fields from the classifier before pickling [@stumpylog](https://github.com/stumpylog) ([#14114](https://github.com/paperless-ngx/paperless-ngx/pull/14114))
- Fix: better catch email workflow placeholder parsing errors [@shamoon](https://github.com/shamoon) ([#14129](https://github.com/paperless-ngx/paperless-ngx/pull/14129))
- Performance: Improves the memory efficiency of classifier training [@stumpylog](https://github.com/stumpylog) ([#14124](https://github.com/paperless-ngx/paperless-ngx/pull/14124))
- Performance: Streams the classifier pickle file during save as well [@stumpylog](https://github.com/stumpylog) ([#14121](https://github.com/paperless-ngx/paperless-ngx/pull/14121))
- Fix: validate legacy bulk edit owner, rotate and split parameters [@stumpylog](https://github.com/stumpylog) ([#14120](https://github.com/paperless-ngx/paperless-ngx/pull/14120))
- Fix: validate set\_permissions with a nested serializer [@stumpylog](https://github.com/stumpylog) ([#14119](https://github.com/paperless-ngx/paperless-ngx/pull/14119))
- Fix: Use prefetching to reduce query counts during classifier training [@stumpylog](https://github.com/stumpylog) ([#14122](https://github.com/paperless-ngx/paperless-ngx/pull/14122))
- Fix: reject non-dict user\_args/barcode\_tag\_mapping in config API [@stumpylog](https://github.com/stumpylog) ([#14118](https://github.com/paperless-ngx/paperless-ngx/pull/14118))
- Fix: type edit\_pdf operations via a nested serializer [@stumpylog](https://github.com/stumpylog) ([#14116](https://github.com/paperless-ngx/paperless-ngx/pull/14116))
- Performance: Loads the classifier through a memory view to reduce memory usage [@stumpylog](https://github.com/stumpylog) ([#14113](https://github.com/paperless-ngx/paperless-ngx/pull/14113))
- Feature: parse advanced search with whoosh-compat and delete the handwritten translation [@stumpylog](https://github.com/stumpylog) ([#14072](https://github.com/paperless-ngx/paperless-ngx/pull/14072))
- Fix: ensure django setup is run for management comments under 3.14 [@shamoon](https://github.com/shamoon) ([#14100](https://github.com/paperless-ngx/paperless-ngx/pull/14100))
- Fix: validate PDF output doc indexes in bulk edit [@shamoon](https://github.com/shamoon) ([#14083](https://github.com/paperless-ngx/paperless-ngx/pull/14083))
- Enhancement: allow regex timeout configuration [@shamoon](https://github.com/shamoon) ([#14085](https://github.com/paperless-ngx/paperless-ngx/pull/14085))
- Fix: avoid IntegrityError when a retried task republishes with the same ID [@stumpylog](https://github.com/stumpylog) ([#14096](https://github.com/paperless-ngx/paperless-ngx/pull/14096))
- Chore: include Apply AI Suggestions in the tasks UI filter dropdown [@shamoon](https://github.com/shamoon) ([#14093](https://github.com/paperless-ngx/paperless-ngx/pull/14093))
- Fix: update some api global perms inconsistencies [@shamoon](https://github.com/shamoon) ([#14086](https://github.com/paperless-ngx/paperless-ngx/pull/14086))
- Fix: ignore nested action IDs on WF create [@shamoon](https://github.com/shamoon) ([#14084](https://github.com/paperless-ngx/paperless-ngx/pull/14084))
- Fix: correct text/stream compression workaround [@shamoon](https://github.com/shamoon) ([#14064](https://github.com/paperless-ngx/paperless-ngx/pull/14064))
- Fix: ui version content switching inconsistencies [@shamoon](https://github.com/shamoon) ([#14066](https://github.com/paperless-ngx/paperless-ngx/pull/14066))
- Fix: prevent saving changes to stale cached document object [@shamoon](https://github.com/shamoon) ([#14065](https://github.com/paperless-ngx/paperless-ngx/pull/14065))
- Performance: batch permission assignment in bulk `set_permissions` [@stumpylog](https://github.com/stumpylog) ([#13806](https://github.com/paperless-ngx/paperless-ngx/pull/13806))
- Performance: skip effective\_content annotation on document list unless required [@stumpylog](https://github.com/stumpylog) ([#13789](https://github.com/paperless-ngx/paperless-ngx/pull/13789))
- Fix: ensure remove inbox tag children on remove\_inbox\_tags [@shamoon](https://github.com/shamoon) ([#14050](https://github.com/paperless-ngx/paperless-ngx/pull/14050))
- Fix: connect add\_to\_index handler after document\_added [@shamoon](https://github.com/shamoon) ([#14058](https://github.com/paperless-ngx/paperless-ngx/pull/14058))
- Enhancement: hide-able sidebar items [@shamoon](https://github.com/shamoon) ([#14052](https://github.com/paperless-ngx/paperless-ngx/pull/14052))
- Performance: cut redundant per-document lookups in bulk `modify_custom_fields` [@stumpylog](https://github.com/stumpylog) ([#13807](https://github.com/paperless-ngx/paperless-ngx/pull/13807))
- Enhancement: Improve matching for correspondents, storage path and labels by removing bias + adding minimum match threshold [@dewey](https://github.com/dewey) ([#12164](https://github.com/paperless-ngx/paperless-ngx/pull/12164))
- Fix: Drop empty files from tracking after the stability window has passed [@stumpylog](https://github.com/stumpylog) ([#14047](https://github.com/paperless-ngx/paperless-ngx/pull/14047))
- Fixhancement: better LLM errors [@shamoon](https://github.com/shamoon) ([#14031](https://github.com/paperless-ngx/paperless-ngx/pull/14031))
- Fix: prevent orphaned versions from bulk delete [@shamoon](https://github.com/shamoon) ([#14030](https://github.com/paperless-ngx/paperless-ngx/pull/14030))
- Enhancement: add Tantivy full-text fallback adapter for taxonomy candidates [@stumpylog](https://github.com/stumpylog) ([#13820](https://github.com/paperless-ngx/paperless-ngx/pull/13820))
- Fixhancement: prevent overlapping mail-account processing runs [@stumpylog](https://github.com/stumpylog) ([#14046](https://github.com/paperless-ngx/paperless-ngx/pull/14046))
- Performance: ensure version-aware content filters on querysets [@shamoon](https://github.com/shamoon) ([#13792](https://github.com/paperless-ngx/paperless-ngx/pull/13792))
- Performance: skip nested TagSerializer construction when a tag has no children [@stumpylog](https://github.com/stumpylog) ([#14039](https://github.com/paperless-ngx/paperless-ngx/pull/14039))
- Enhancement (QoL): surface externally-set options in Config UI [@shamoon](https://github.com/shamoon) ([#13989](https://github.com/paperless-ngx/paperless-ngx/pull/13989))
- Enhancement: allow disabling auto-suggestions for inbox documents [@shamoon](https://github.com/shamoon) ([#13946](https://github.com/paperless-ngx/paperless-ngx/pull/13946))
- Change: skip documents with empty content in apply AI suggestions WF [@shamoon](https://github.com/shamoon) ([#13985](https://github.com/paperless-ngx/paperless-ngx/pull/13985))
- Performance: resolve index-write permissions and effective content in bulk [@stumpylog](https://github.com/stumpylog) ([#13869](https://github.com/paperless-ngx/paperless-ngx/pull/13869))
- Fix: Use PAPERLESS\_REDIS\_PREFIX for Celery result backend keys [@bdd](https://github.com/bdd) ([#14015](https://github.com/paperless-ngx/paperless-ngx/pull/14015))
- Enhancement: duplicates filter [@shamoon](https://github.com/shamoon) ([#13994](https://github.com/paperless-ngx/paperless-ngx/pull/13994))
- Fix: correct setting ai\_enabled to false via UI [@shamoon](https://github.com/shamoon) ([#13987](https://github.com/paperless-ngx/paperless-ngx/pull/13987))
- Chore(deps): Bump the utilities-minor group across 1 directory with 11 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13988](https://github.com/paperless-ngx/paperless-ngx/pull/13988))
- Fix: catch some frontend failed object retrievals [@shamoon](https://github.com/shamoon) ([#14023](https://github.com/paperless-ngx/paperless-ngx/pull/14023))
- Fix: more v3 icons cleanup [@shamoon](https://github.com/shamoon) ([#14017](https://github.com/paperless-ngx/paperless-ngx/pull/14017))
- Fix: correct add version actor parity [@shamoon](https://github.com/shamoon) ([#14016](https://github.com/paperless-ngx/paperless-ngx/pull/14016))
- Fix: fix v3 favicon file [@shamoon](https://github.com/shamoon) ([#14014](https://github.com/paperless-ngx/paperless-ngx/pull/14014))
- Tweak: note that apply AI suggestions runs async in WF editor [@shamoon](https://github.com/shamoon) ([#14004](https://github.com/paperless-ngx/paperless-ngx/pull/14004))
- Fix: change share link bundle dialog button to close after create, don't toast on copied [@shamoon](https://github.com/shamoon) ([#14002](https://github.com/paperless-ngx/paperless-ngx/pull/14002))
- Enhancement (QoL): attempt to localize firstDayOfWeek for date picker [@shamoon](https://github.com/shamoon) ([#13999](https://github.com/paperless-ngx/paperless-ngx/pull/13999))
- Fix: truncate mail subjects to field max length [@shamoon](https://github.com/shamoon) ([#13991](https://github.com/paperless-ngx/paperless-ngx/pull/13991))
- Chore(deps): Bump sentence-transformers from 5.6.1 to 6.0.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#13983](https://github.com/paperless-ngx/paperless-ngx/pull/13983))
- Chore(deps-dev): Bump types-markdown from 3.10.2.20260518 to 3.10.2.20260712 @[dependabot[bot]](https://github.com/apps/dependabot) ([#13982](https://github.com/paperless-ngx/paperless-ngx/pull/13982))
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13980](https://github.com/paperless-ngx/paperless-ngx/pull/13980))
- Chore(deps): Update granian[uvloop] requirement from ~=2.7.0 to >=2.7,\<2.9 @[dependabot[bot]](https://github.com/apps/dependabot) ([#13984](https://github.com/paperless-ngx/paperless-ngx/pull/13984))
- Chore: Updates our direct Redis pin [@stumpylog](https://github.com/stumpylog) ([#13986](https://github.com/paperless-ngx/paperless-ngx/pull/13986))
- Fix: enforce the overflow hidden rule on pdf editor thumbnails [@shamoon](https://github.com/shamoon) ([#13976](https://github.com/paperless-ngx/paperless-ngx/pull/13976))
- Fix: also correct unbroken long names on small cards [@shamoon](https://github.com/shamoon) ([#13974](https://github.com/paperless-ngx/paperless-ngx/pull/13974))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13918](https://github.com/paperless-ngx/paperless-ngx/pull/13918))
- Chore(deps): Bump uuid from 14.0.1 to 14.0.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13921](https://github.com/paperless-ngx/paperless-ngx/pull/13921))
- Chore(deps-dev): Bump @types/node from 26.2.0 to 26.4.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13919](https://github.com/paperless-ngx/paperless-ngx/pull/13919))
- Chore: update ng-select to v24, handle breaking changes [@shamoon](https://github.com/shamoon) ([#13951](https://github.com/paperless-ngx/paperless-ngx/pull/13951))
- Chore(deps): Bump djangorestframework from 3.17.2 to 3.18.0 in the django-ecosystem group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13912](https://github.com/paperless-ngx/paperless-ngx/pull/13912))
- Chore(deps): Bump the document-processing group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13916](https://github.com/paperless-ngx/paperless-ngx/pull/13916))
- Chore(deps): Bump flower from 2.0.1 to 2.1.0 in the async-tasks group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13913](https://github.com/paperless-ngx/paperless-ngx/pull/13913))
- Chore(deps-dev): Bump the development group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13911](https://github.com/paperless-ngx/paperless-ngx/pull/13911))
- Fix: ensure parent + child tags change together in bulk editor [@shamoon](https://github.com/shamoon) ([#13972](https://github.com/paperless-ngx/paperless-ngx/pull/13972))
</details>
## paperless-ngx 3.1.3 ## paperless-ngx 3.1.3
### Bug Fixes ### Bug Fixes
+20
View File
@@ -2133,6 +2133,13 @@ for language and resource considerations.
Defaults to None. Defaults to None.
#### [`PAPERLESS_AI_LLM_EMBEDDING_API_KEY=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_API_KEY) {#PAPERLESS_AI_LLM_EMBEDDING_API_KEY}
: The API key to use for the embedding backend. If not supplied, embeddings use
`PAPERLESS_AI_LLM_API_KEY`.
Defaults to None.
#### [`PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT) {#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT} #### [`PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT) {#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT}
: The endpoint / url to use for the embedding backend. If not supplied, embeddings use : The endpoint / url to use for the embedding backend. If not supplied, embeddings use
@@ -2217,6 +2224,19 @@ used with the OpenAI-compatible backend to target a custom provider or local gat
Defaults to true, which allows internal endpoints. Defaults to true, which allows internal endpoints.
#### [`PAPERLESS_AI_LLM_EXTRA_PARAMS=<json>`](#PAPERLESS_AI_LLM_EXTRA_PARAMS) {#PAPERLESS_AI_LLM_EXTRA_PARAMS}
: A JSON object of extra parameters sent with every LLM request, for providers that require a parameter Paperless does not
set itself. Values here override Paperless' own, and no validation is performed. Whatever you put here is passed to the
backend as-is, so an invalid parameter will simply be rejected by your provider. For example, current OpenAI reasoning
models refuse tool calls on the chat completions API unless reasoning is off:
```
PAPERLESS_AI_LLM_EXTRA_PARAMS={"reasoning_effort": "none"}
```
Defaults to empty, which adds nothing to requests.
#### [`PAPERLESS_LLM_INDEX_TASK_CRON=<cron expression>`](#PAPERLESS_LLM_INDEX_TASK_CRON) {#PAPERLESS_LLM_INDEX_TASK_CRON} #### [`PAPERLESS_LLM_INDEX_TASK_CRON=<cron expression>`](#PAPERLESS_LLM_INDEX_TASK_CRON) {#PAPERLESS_LLM_INDEX_TASK_CRON}
: Configures the schedule to update the AI embeddings of text content and metadata for all documents. Only performed if : Configures the schedule to update the AI embeddings of text content and metadata for all documents. Only performed if
+1
View File
@@ -150,6 +150,7 @@ pnpm ng build --configuration production
is loaded as well. However, the tests rely on the default is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing. except for DEBUG are overridden when testing.
- Tests run in a random order each session, so that one test cannot quietly depend on another having run first. The seed is printed at the top of the run; pass `--randomly-seed=<seed>` to replay that exact order, or `--randomly-seed=last` to repeat the previous run.
!!! note !!! note
+25 -14
View File
@@ -1,7 +1,9 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "3.2.0" version = "3.2.1"
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"
classifiers = [ classifiers = [
@@ -10,13 +12,13 @@ classifiers = [
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
] ]
# TODO: Move certain things to groups and then utilize that further # TODO: Move certain things to groups and then utilize that further
# This will allow testing to not install a webserver, mysql, etc # This will allow testing to not install a webserver, mysql, etc
dependencies = [ dependencies = [
"azure-ai-documentintelligence>=1.0.2", "azure-ai-documentintelligence>=1.0.2",
"babel>=2.17", "babel>=2.17",
"bleach~=6.4.0",
"celery[redis]~=5.6.2", "celery[redis]~=5.6.2",
"channels~=4.2", "channels~=4.2",
"channels-redis~=4.2", "channels-redis~=4.2",
@@ -32,7 +34,7 @@ dependencies = [
"django-cors-headers~=4.9.0", "django-cors-headers~=4.9.0",
"django-extensions~=4.1", "django-extensions~=4.1",
"django-filter~=25.1", "django-filter~=25.1",
"django-guardian>=3.3.3,<3.5", "django-guardian>=3.3.3,<3.6",
"django-multiselectfield~=1.0.1", "django-multiselectfield~=1.0.1",
"django-rich~=2.2.0", "django-rich~=2.2.0",
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
@@ -42,7 +44,7 @@ dependencies = [
"drf-spectacular-sidecar>=2026.7.1,<2026.10", "drf-spectacular-sidecar>=2026.7.1,<2026.10",
"drf-writable-nested~=0.7.1", "drf-writable-nested~=0.7.1",
"filelock~=3.32.0", "filelock~=3.32.0",
"flower>=2.0.1,<2.2", "flower>=2.0.1,<2.3",
"gotenberg-client[httpx]~=1.0", "gotenberg-client[httpx]~=1.0",
"httpx-oauth~=0.17", "httpx-oauth~=0.17",
"ijson>=3.5.1", "ijson>=3.5.1",
@@ -55,7 +57,7 @@ dependencies = [
"llama-index-embeddings-openai-like>=0.2.2", "llama-index-embeddings-openai-like>=0.2.2",
"llama-index-llms-ollama>=0.9.1", "llama-index-llms-ollama>=0.9.1",
"llama-index-llms-openai-like>=0.7.1", "llama-index-llms-openai-like>=0.7.1",
"ocrmypdf>=17.7,<17.12", "ocrmypdf[heic]>=17.12,<17.13",
"openai>=2.48", "openai>=2.48",
"pathvalidate~=3.3.1", "pathvalidate~=3.3.1",
"pdf2image~=1.17.0", "pdf2image~=1.17.0",
@@ -74,6 +76,7 @@ dependencies = [
"tantivy~=0.26.0", "tantivy~=0.26.0",
"tika-client[httpx]~=1.0", "tika-client[httpx]~=1.0",
"torch>=2.13,<2.15", "torch>=2.13,<2.15",
"turbohtml~=1.10.0",
"watchfiles>=1.2", "watchfiles>=1.2",
"whitenoise~=6.11", "whitenoise~=6.11",
"whoosh-compat[tantivy]==0.3", "whoosh-compat[tantivy]==0.3",
@@ -81,13 +84,13 @@ dependencies = [
] ]
[project.optional-dependencies] [project.optional-dependencies]
mariadb = [ mariadb = [
"mysqlclient~=2.2.7", "mysqlclient>=2.2.7,<2.4",
] ]
postgres = [ postgres = [
"psycopg[c,pool]==3.3.4", "psycopg[c,pool]==3.3.4",
# Direct dependency for proper resolution of the pre-built wheels # Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.3.4", "psycopg-c==3.3.4",
"psycopg-pool==3.3.1", "psycopg-pool==3.3.2",
] ]
webserver = [ webserver = [
"granian[uvloop]>=2.7,<2.9", "granian[uvloop]>=2.7,<2.9",
@@ -109,7 +112,7 @@ lint = [
testing = [ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"faker>=40.36,<40.39", "faker>=40.36,<40.40",
"imagehash", "imagehash",
"pytest~=9.1.1", "pytest~=9.1.1",
"pytest-cov~=7.1.0", "pytest-cov~=7.1.0",
@@ -117,7 +120,7 @@ testing = [
"pytest-env~=1.7.0", "pytest-env~=1.7.0",
"pytest-httpx", "pytest-httpx",
"pytest-mock~=3.15.1", "pytest-mock~=3.15.1",
# "pytest-randomly~=4.0.1", "pytest-randomly~=5.0.0",
"pytest-rerunfailures~=16.4", "pytest-rerunfailures~=16.4",
"pytest-sugar", "pytest-sugar",
"pytest-xdist~=3.8.0", "pytest-xdist~=3.8.0",
@@ -133,7 +136,6 @@ typing = [
"mypy", "mypy",
"mypy-baseline", "mypy-baseline",
"pyrefly", "pyrefly",
"types-bleach",
"types-channels", "types-channels",
"types-colorama", "types-colorama",
"types-dateparser", "types-dateparser",
@@ -148,6 +150,7 @@ typing = [
[tool.uv] [tool.uv]
required-version = ">=0.9.0" required-version = ">=0.9.0"
prerelease = "disallow"
environments = [ environments = [
"sys_platform == 'darwin'", "sys_platform == 'darwin'",
"sys_platform == 'linux'", "sys_platform == 'linux'",
@@ -244,6 +247,10 @@ per-file-ignores."docker/wait-for-redis.py" = [
per-file-ignores."src/documents/models.py" = [ per-file-ignores."src/documents/models.py" = [
"SIM115", "SIM115",
] ]
per-file-ignores."src/documents/tests/*.py" = [
"TID251",
]
flake8-tidy-imports.banned-api."documents.tests".msg = "Shared test infrastructure lives in src/paperless_testing/."
isort.force-single-line = true isort.force-single-line = true
[tool.codespell] [tool.codespell]
@@ -271,9 +278,9 @@ plugins = [
] ]
[tool.pyrefly] [tool.pyrefly]
baseline = ".pyrefly-baseline.json"
python-platform = "linux" python-platform = "linux"
search-path = [ "src" ] search-path = [ "src" ]
baseline = ".pyrefly-baseline.json"
[tool.django-stubs] [tool.django-stubs]
django_settings_module = "paperless.settings" django_settings_module = "paperless.settings"
@@ -326,6 +333,8 @@ PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
PAPERLESS_CHANNELS_BACKEND = "channels.layers.InMemoryChannelLayer" PAPERLESS_CHANNELS_BACKEND = "channels.layers.InMemoryChannelLayer"
# I don't think anything hits this, but just in case, basically infinite # I don't think anything hits this, but just in case, basically infinite
PAPERLESS_TOKEN_THROTTLE_RATE = "1000/min" PAPERLESS_TOKEN_THROTTLE_RATE = "1000/min"
# The 0.1s production default trips on a stalled CI runner, the date parsing tests then find no dates
PAPERLESS_MATCH_REGEX_TIMEOUT_SECONDS = "5"
[tool.coverage.run] [tool.coverage.run]
source = [ source = [
@@ -334,13 +343,15 @@ source = [
omit = [ omit = [
"*/tests/*", "*/tests/*",
"manage.py", "manage.py",
"paperless/wsgi.py",
"paperless/auth.py", "paperless/auth.py",
"paperless/wsgi.py",
"src/conftest.py",
"src/paperless_testing/*",
] ]
[tool.coverage.report] [tool.coverage.report]
exclude_also = [ exclude_also = [
"if settings.AUDIT_LOG_ENABLED:",
"if AUDIT_LOG_ENABLED:", "if AUDIT_LOG_ENABLED:",
"if settings.AUDIT_LOG_ENABLED:",
"if TYPE_CHECKING:", "if TYPE_CHECKING:",
] ]
+5
View File
@@ -91,6 +91,11 @@
"glob": "**/*", "glob": "**/*",
"input": "node_modules/pdfjs-dist/iccs/", "input": "node_modules/pdfjs-dist/iccs/",
"output": "/assets/iccs/" "output": "/assets/iccs/"
},
{
"glob": "**/*",
"input": "node_modules/pdfjs-dist/cmaps/",
"output": "/assets/cmaps/"
} }
], ],
"styles": [ "styles": [
+315 -238
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "3.2.0", "version": "3.2.1",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"ng": "ng", "ng": "ng",
@@ -15,16 +15,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^22.1.6", "@angular/cdk": "^22.2.0",
"@angular/common": "~22.1.6", "@angular/common": "~22.2.0",
"@angular/compiler": "~22.1.6", "@angular/compiler": "~22.2.0",
"@angular/core": "~22.1.6", "@angular/core": "~22.2.0",
"@angular/forms": "~22.1.6", "@angular/forms": "~22.2.0",
"@angular/localize": "~22.1.6", "@angular/localize": "~22.2.0",
"@angular/platform-browser": "~22.1.6", "@angular/platform-browser": "~22.2.0",
"@angular/router": "~22.1.6", "@angular/router": "~22.2.0",
"@ng-bootstrap/ng-bootstrap": "^21.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~24.1.1", "@ng-select/ng-select": "~24.1.2",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
@@ -45,29 +45,29 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "^22.0.1", "@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.1.8", "@angular-devkit/core": "^22.2.0",
"@angular-devkit/schematics": "^22.1.8", "@angular-devkit/schematics": "^22.2.0",
"@angular-eslint/builder": "22.5.0", "@angular-eslint/builder": "22.5.0",
"@angular-eslint/eslint-plugin": "22.5.0", "@angular-eslint/eslint-plugin": "22.5.0",
"@angular-eslint/eslint-plugin-template": "22.5.0", "@angular-eslint/eslint-plugin-template": "22.5.0",
"@angular-eslint/schematics": "22.5.0", "@angular-eslint/schematics": "22.5.0",
"@angular-eslint/template-parser": "22.5.0", "@angular-eslint/template-parser": "22.5.0",
"@angular/build": "22.1.8", "@angular/build": "22.2.0",
"@angular/cli": "22.1.8", "@angular/cli": "22.2.0",
"@angular/compiler-cli": "~22.1.6", "@angular/compiler-cli": "~22.2.0",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.5.0", "@types/node": "^26.6.2",
"@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.70.0", "@typescript-eslint/parser": "^8.70.1",
"@typescript-eslint/utils": "^8.70.0", "@typescript-eslint/utils": "^8.70.1",
"eslint": "^10.10.0", "eslint": "^10.11.0",
"jest": "30.5.1", "jest": "30.5.2",
"jest-environment-jsdom": "^30.5.1", "jest-environment-jsdom": "^30.5.2",
"jest-junit": "^17.0.0", "jest-junit": "^17.0.0",
"jest-preset-angular": "^17.0.0", "jest-preset-angular": "^17.0.0",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier": "^3.9.6", "prettier": "^3.9.9",
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.2", "ts-node": "~10.9.2",
"typescript": "^6.0.3" "typescript": "^6.0.3"
+1312 -1960
View File
File diff suppressed because it is too large Load Diff
@@ -253,6 +253,24 @@
</div> </div>
</div> </div>
@if (aiEnabled) {
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Suggestions default to</span>
</div>
<div class="col">
<fieldset class="btn-group btn-group-sm">
<input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceML"><i-bs class="me-1" name="cpu"></i-bs><ng-container i18n>ML only</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceAI"><i-bs class="me-1" name="stars"></i-bs><ng-container i18n>AI only</ng-container></label>
</fieldset>
</div>
</div>
}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-check i18n-title title="Automatically request suggestions for inbox documents" i18n-hint hint="If un-checked, suggestions must be requested via the Suggest button." formControlName="documentEditingAutoSuggest"></pngx-input-check> <pngx-input-check i18n-title title="Automatically request suggestions for inbox documents" i18n-hint hint="If un-checked, suggestions must be requested via the Suggest button." formControlName="documentEditingAutoSuggest"></pngx-input-check>
@@ -307,7 +307,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(34) expect(setSpy).toHaveBeenCalledTimes(35)
expect(setSpy).toHaveBeenCalledWith(SETTINGS_KEYS.SIDEBAR_HIDDEN_ITEMS, [ expect(setSpy).toHaveBeenCalledWith(SETTINGS_KEYS.SIDEBAR_HIDDEN_ITEMS, [
HideableSidebarItemID.Workflows, HideableSidebarItemID.Workflows,
]) ])
@@ -44,6 +44,7 @@ import {
HIDEABLE_SIDEBAR_ITEM_IDS, HIDEABLE_SIDEBAR_ITEM_IDS,
HideableSidebarItemID, HideableSidebarItemID,
SETTINGS_KEYS, SETTINGS_KEYS,
SuggestionSource,
} from 'src/app/data/ui-settings' } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -184,6 +185,7 @@ export class SettingsComponent
documentEditingRemoveInboxTags: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null),
documentEditingOverlayThumbnail: new FormControl(null), documentEditingOverlayThumbnail: new FormControl(null),
documentEditingAutoSuggest: new FormControl(null), documentEditingAutoSuggest: new FormControl(null),
documentEditingSuggestionSource: new FormControl(null),
documentDetailsHiddenFields: new FormControl([]), documentDetailsHiddenFields: new FormControl([]),
searchDbOnly: new FormControl(null), searchDbOnly: new FormControl(null),
searchLink: new FormControl(null), searchLink: new FormControl(null),
@@ -217,6 +219,11 @@ export class SettingsComponent
public readonly PdfZoomScale = PdfZoomScale public readonly PdfZoomScale = PdfZoomScale
public readonly PdfEditorEditMode = PdfEditorEditMode public readonly PdfEditorEditMode = PdfEditorEditMode
public readonly SuggestionSource = SuggestionSource
get aiEnabled(): boolean {
return this.settings.get(SETTINGS_KEYS.AI_ENABLED)
}
public readonly documentDetailFieldOptions = documentDetailFieldOptions public readonly documentDetailFieldOptions = documentDetailFieldOptions
public readonly sidebarItemOptions = HIDEABLE_SIDEBAR_ITEM_IDS.map((id) => ({ public readonly sidebarItemOptions = HIDEABLE_SIDEBAR_ITEM_IDS.map((id) => ({
@@ -404,6 +411,9 @@ export class SettingsComponent
documentEditingAutoSuggest: this.settings.get( documentEditingAutoSuggest: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST
), ),
documentEditingSuggestionSource: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE
),
documentDetailsHiddenFields: this.settings.get( documentDetailsHiddenFields: this.settings.get(
SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS
), ),
@@ -625,6 +635,10 @@ export class SettingsComponent
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST,
this.settingsForm.value.documentEditingAutoSuggest this.settingsForm.value.documentEditingAutoSuggest
) )
this.settings.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
this.settingsForm.value.documentEditingSuggestionSource
)
this.settings.set( this.settings.set(
SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS,
this.settingsForm.value.documentDetailsHiddenFields this.settingsForm.value.documentDetailsHiddenFields
@@ -154,11 +154,29 @@
& section { & section {
position: absolute; position: absolute;
text-align: initial; text-align: initial;
pointer-events: auto;
box-sizing: border-box; box-sizing: border-box;
transform-origin: 0 0; transform-origin: 0 0;
} }
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
position: absolute;
inset: 0;
font-size: 1em;
transition: none;
}
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
> a:hover {
opacity: 0.2;
background-color: rgb(255 255 0);
}
& .annotationTextContent { & .annotationTextContent {
opacity: 0; opacity: 0;
} }
} }
:host ::ng-deep .textLayer.selecting ~ .annotationLayer section {
pointer-events: none;
}
@@ -1,7 +1,11 @@
import { SimpleChange } from '@angular/core' import { SimpleChange } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs' import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs' import {
LinkTarget,
PDFSinglePageViewer,
PDFViewer,
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import { PngxPdfViewerComponent } from './pdf-viewer.component' import { PngxPdfViewerComponent } from './pdf-viewer.component'
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types' import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
@@ -58,6 +62,16 @@ describe('PngxPdfViewerComponent', () => {
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer) expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
}) })
it('opens external links in a new tab', () => {
const linkService = (component as any).linkService
expect(linkService.options).toEqual(
expect.objectContaining({
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener noreferrer nofollow',
})
)
})
it('resolves the worker source relative to the document base URI', async () => { it('resolves the worker source relative to the document base URI', async () => {
setBaseHref('/paperless/') setBaseHref('/paperless/')
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument') const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
@@ -76,6 +90,7 @@ describe('PngxPdfViewerComponent', () => {
withCredentials: true, withCredentials: true,
wasmUrl: expect.stringContaining('/paperless/assets/wasm/'), wasmUrl: expect.stringContaining('/paperless/assets/wasm/'),
iccUrl: expect.stringContaining('/paperless/assets/iccs/'), iccUrl: expect.stringContaining('/paperless/assets/iccs/'),
cMapUrl: expect.stringContaining('/paperless/assets/cmaps/'),
}) })
}) })
@@ -21,6 +21,7 @@ import {
} from 'pdfjs-dist/legacy/build/pdf.mjs' } from 'pdfjs-dist/legacy/build/pdf.mjs'
import { import {
EventBus, EventBus,
LinkTarget,
PDFFindController, PDFFindController,
PDFLinkService, PDFLinkService,
PDFSinglePageViewer, PDFSinglePageViewer,
@@ -75,7 +76,11 @@ export class PngxPdfViewerComponent
private lastViewerPage?: number private lastViewerPage?: number
private readonly eventBus = new EventBus() private readonly eventBus = new EventBus()
private readonly linkService = new PDFLinkService({ eventBus: this.eventBus }) private readonly linkService = new PDFLinkService({
eventBus: this.eventBus,
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener noreferrer nofollow',
})
private readonly findController = new PDFFindController({ private readonly findController = new PDFFindController({
eventBus: this.eventBus, eventBus: this.eventBus,
linkService: this.linkService, linkService: this.linkService,
@@ -185,6 +190,7 @@ export class PngxPdfViewerComponent
withCredentials: true, withCredentials: true,
wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(), wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(),
iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(), iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(),
cMapUrl: new URL('assets/cmaps/', this.document.baseURI).toString(),
} }
this.loadingTask = getDocument(initOptions) this.loadingTask = getDocument(initOptions)
try { try {
@@ -1,58 +1,84 @@
<div class="btn-group"> <div class="d-flex align-items-center">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())" [aria-label]="noSuggestions ? 'No suggestions' : 'Suggest'" i18n-aria-label> <div class="btn-group">
@if (loading()) { <button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())" [aria-label]="noSuggestions ? 'No suggestions' : 'Suggest'" i18n-aria-label>
<div class="spinner-border spinner-border-sm" role="status"></div> @if (loading()) {
} @else if (noSuggestions) { <div class="spinner-border spinner-border-sm" role="status"></div>
<i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs> } @else if (noSuggestions) {
} @else { <i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs>
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs> } @else {
<i-bs width="1.2em" height="1.2em" name="lightbulb"></i-bs>
}
@if (noSuggestions) {
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
} @else {
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
}
@if (totalSuggestions > 0) {
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
}
</button>
@if (aiEnabled()) {
<div class="btn-group" ngbDropdown #dropdown="ngbDropdown" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-outline-primary" ngbDropdownToggle [disabled]="disabled() || loading() || !suggestions()" aria-expanded="false" aria-controls="suggestionsDropdown" aria-label="Suggestions dropdown">
<span class="visually-hidden" i18n>Show suggestions</span>
</button>
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
<div class="list-group list-group-flush small pb-0">
@if (novelSuggestions === 0 && reusableSuggestions === 0) {
<div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
</div>
}
@if (suggestions()?.suggested_tags?.length > 0) {
<small class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="tags"></i-bs><ng-container i18n>Tags</ng-container></small>
@for (tag of suggestions().suggested_tags; track tag) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)">{{ tag }}</button>
}
}
@if (suggestions()?.suggested_document_types?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="hash"></i-bs><ng-container i18n>Document Types</ng-container></div>
@for (type of suggestions().suggested_document_types; track type) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)">{{ type }}</button>
}
}
@if (suggestions()?.suggested_correspondents?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="person"></i-bs><ng-container i18n>Correspondents</ng-container></div>
@for (correspondent of suggestions().suggested_correspondents; track correspondent) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
}
}
@if (reusableSuggestions > 0) {
<div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>{reusableSuggestions, plural, =1 {1 existing value suggested below} other {{{reusableSuggestions}} existing values suggested below}}</small>
</div>
}
</div>
</div>
</div>
} }
@if (noSuggestions) { </div>
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
} @else {
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
}
@if (totalSuggestions > 0) {
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
}
</button>
@if (aiEnabled()) { @if (aiEnabled()) {
<div class="btn-group" ngbDropdown #dropdown="ngbDropdown" [popperOptions]="popperOptions"> <div ngbDropdown autoClose="outside" placement="bottom-end" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-outline-primary" ngbDropdownToggle [disabled]="disabled() || loading() || !suggestions()" aria-expanded="false" aria-controls="suggestionsDropdown" aria-label="Suggestions dropdown"> <button type="button" class="btn btn-sm btn-link position-relative" ngbDropdownToggle [disabled]="disabled() || loading()" i18n-title title="Suggestion options">
<span class="visually-hidden" i18n>Show suggestions</span> <i-bs name="three-dots"></i-bs>
@if (source() !== defaultSource()) {
<span class="position-absolute top-0 start-100 translate-middle p-1 bg-primary border border-light rounded-circle">
<span class="visually-hidden" i18n>Not using default</span>
</span>
}
</button> </button>
<div ngbDropdownMenu class="shadow p-3">
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown"> <div class="small text-muted mb-2" i18n>Suggest using:</div>
<div class="list-group list-group-flush small pb-0"> <div class="form-check small">
@if (novelSuggestions === 0 && reusableSuggestions === 0) { <input class="form-check-input" type="checkbox" id="suggestionSourceML" [checked]="useML" [disabled]="useML && !useAI" (change)="setSources($event.target.checked, useAI)">
<div class="list-group-item text-muted fst-italic"> <label class="form-check-label d-inline-flex align-items-center gap-1" for="suggestionSourceML"><i-bs name="cpu"></i-bs><ng-container i18n>ML</ng-container></label>
<small class="text-muted small fst-italic" i18n>No novel suggestions</small> </div>
</div> <div class="form-check small">
} <input class="form-check-input" type="checkbox" id="suggestionSourceAI" [checked]="useAI" [disabled]="useAI && !useML" (change)="setSources(useML, $event.target.checked)">
@if (suggestions()?.suggested_tags?.length > 0) { <label class="form-check-label d-inline-flex align-items-center gap-1" for="suggestionSourceAI"><i-bs name="stars"></i-bs><ng-container i18n>AI</ng-container></label>
<small class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="tags"></i-bs><ng-container i18n>Tags</ng-container></small>
@for (tag of suggestions().suggested_tags; track tag) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)">{{ tag }}</button>
}
}
@if (suggestions()?.suggested_document_types?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="hash"></i-bs><ng-container i18n>Document Types</ng-container></div>
@for (type of suggestions().suggested_document_types; track type) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)">{{ type }}</button>
}
}
@if (suggestions()?.suggested_correspondents?.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="person"></i-bs><ng-container i18n>Correspondents</ng-container></div>
@for (correspondent of suggestions().suggested_correspondents; track correspondent) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
}
}
@if (reusableSuggestions > 0) {
<div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>{reusableSuggestions, plural, =1 {1 existing value suggested below} other {{{reusableSuggestions}} existing values suggested below}}</small>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -1,3 +1,7 @@
.suggestions-dropdown { .suggestions-dropdown {
min-width: 250px; min-width: 250px;
} }
.btn-link.dropdown-toggle::after {
display: none;
}
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SuggestionSource } from 'src/app/data/ui-settings'
import { SuggestionsDropdownComponent } from './suggestions-dropdown.component' import { SuggestionsDropdownComponent } from './suggestions-dropdown.component'
describe('SuggestionsDropdownComponent', () => { describe('SuggestionsDropdownComponent', () => {
@@ -179,14 +180,71 @@ describe('SuggestionsDropdownComponent', () => {
it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => { it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => {
fixture.componentRef.setInput('aiEnabled', true) fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.detectChanges() fixture.detectChanges()
fixture.componentRef.setInput('suggestions', { fixture.componentRef.setInput('suggestions', {
suggested_correspondents: [], suggested_correspondents: [],
suggested_tags: [], suggested_tags: [],
suggested_document_types: [], suggested_document_types: [],
}) })
fixture.detectChanges()
component.clickSuggest() component.clickSuggest()
expect(component.dropdown.open).toBeTruthy() expect(component.dropdown.isOpen()).toBeTruthy()
expect(fixture.nativeElement.textContent).toContain('No novel suggestions') expect(fixture.nativeElement.textContent).toContain('No novel suggestions')
}) })
it('should fetch unfetched sources and show existing suggestions', () => {
jest.spyOn(component.getSuggestions, 'emit')
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.componentRef.setInput('suggestions', { tags: [1] })
fixture.detectChanges()
component.clickSuggest()
expect(component.getSuggestions.emit).toHaveBeenCalledWith(
SuggestionSource.Both
)
expect(component.dropdown.isOpen()).toBeTruthy()
})
it('should only show source options when AI is enabled', () => {
expect(
fixture.nativeElement.querySelector('#suggestionSourceML')
).toBeNull()
fixture.componentRef.setInput('aiEnabled', true)
fixture.detectChanges()
fixture.nativeElement
.querySelector('button[title="Suggestion options"]')
.click()
fixture.detectChanges()
expect(
fixture.nativeElement.querySelector('#suggestionSourceML')
).not.toBeNull()
})
it('should emit source changes and never allow no source', () => {
const emitSpy = jest.spyOn(component.sourceChange, 'emit')
component.setSources(true, true)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.Both)
component.setSources(true, false)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.ML)
component.setSources(false, true)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.AI)
emitSpy.mockClear()
component.setSources(false, false)
expect(emitSpy).not.toHaveBeenCalled()
})
it('should indicate a non-default source', () => {
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.AI)
fixture.componentRef.setInput('defaultSource', SuggestionSource.AI)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).not.toContain('Not using default')
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Not using default')
})
}) })
@@ -8,6 +8,7 @@ import {
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentSuggestions } from 'src/app/data/document-suggestions' import { DocumentSuggestions } from 'src/app/data/document-suggestions'
import { SuggestionSource } from 'src/app/data/ui-settings'
import { pngxPopperOptions } from 'src/app/utils/popper-options' import { pngxPopperOptions } from 'src/app/utils/popper-options'
@Component({ @Component({
@@ -18,12 +19,16 @@ import { pngxPopperOptions } from 'src/app/utils/popper-options'
}) })
export class SuggestionsDropdownComponent { export class SuggestionsDropdownComponent {
public popperOptions = pngxPopperOptions public popperOptions = pngxPopperOptions
public readonly SuggestionSource = SuggestionSource
@ViewChild('dropdown') dropdown: NgbDropdown @ViewChild('dropdown') dropdown: NgbDropdown
readonly suggestions = input<DocumentSuggestions>(null) readonly suggestions = input<DocumentSuggestions>(null)
readonly aiEnabled = input(false) readonly aiEnabled = input(false)
readonly loading = input(false) readonly loading = input(false)
readonly disabled = input(false) readonly disabled = input(false)
readonly source = input<SuggestionSource>(SuggestionSource.ML)
readonly defaultSource = input<SuggestionSource>(SuggestionSource.ML)
readonly fetchedSources = input<SuggestionSource[]>([])
readonly appliedTags = input<number[]>([]) readonly appliedTags = input<number[]>([])
readonly appliedCorrespondent = input<number>(null) readonly appliedCorrespondent = input<number>(null)
@@ -31,8 +36,10 @@ export class SuggestionsDropdownComponent {
readonly appliedStoragePath = input<number>(null) readonly appliedStoragePath = input<number>(null)
@Output() @Output()
getSuggestions: EventEmitter<SuggestionsDropdownComponent> = getSuggestions: EventEmitter<SuggestionSource> = new EventEmitter()
new EventEmitter()
@Output()
sourceChange: EventEmitter<SuggestionSource> = new EventEmitter()
@Output() @Output()
addTag: EventEmitter<string> = new EventEmitter() addTag: EventEmitter<string> = new EventEmitter()
@@ -53,12 +60,42 @@ export class SuggestionsDropdownComponent {
} }
if (!this.suggestions()) { if (!this.suggestions()) {
this.getSuggestions.emit(this) this.getSuggestions.emit(this.source())
} else if (this.hasUnfetchedSources) {
// sources changed, fetch the rest and show what we have meanwhile
this.getSuggestions.emit(this.source())
this.dropdown?.open()
} else { } else {
this.dropdown?.toggle() this.dropdown?.toggle()
} }
} }
get useML(): boolean {
return this.source() !== SuggestionSource.AI
}
get useAI(): boolean {
return this.source() !== SuggestionSource.ML
}
get hasUnfetchedSources(): boolean {
const fetched = this.fetchedSources()
return (
(this.useML && !fetched.includes(SuggestionSource.ML)) ||
(this.useAI && !fetched.includes(SuggestionSource.AI))
)
}
public setSources(ml: boolean, ai: boolean) {
if (ml && ai) {
this.sourceChange.emit(SuggestionSource.Both)
} else if (ml) {
this.sourceChange.emit(SuggestionSource.ML)
} else if (ai) {
this.sourceChange.emit(SuggestionSource.AI)
}
}
get novelSuggestions(): number { get novelSuggestions(): number {
return ( return (
(this.suggestions()?.suggested_correspondents?.length ?? 0) + (this.suggestions()?.suggested_correspondents?.length ?? 0) +
@@ -134,11 +134,15 @@
[loading]="suggestionsLoading()" [loading]="suggestionsLoading()"
[suggestions]="suggestions()" [suggestions]="suggestions()"
[aiEnabled]="aiEnabled" [aiEnabled]="aiEnabled"
[source]="suggestionSource"
[defaultSource]="defaultSuggestionSource"
[fetchedSources]="fetchedSuggestionSources()"
[appliedTags]="documentForm.value.tags" [appliedTags]="documentForm.value.tags"
[appliedCorrespondent]="documentForm.value.correspondent" [appliedCorrespondent]="documentForm.value.correspondent"
[appliedDocumentType]="documentForm.value.document_type" [appliedDocumentType]="documentForm.value.document_type"
[appliedStoragePath]="documentForm.value.storage_path" [appliedStoragePath]="documentForm.value.storage_path"
(getSuggestions)="getSuggestions()" (getSuggestions)="getSuggestions($event)"
(sourceChange)="suggestionSourceOverride.set($event)"
(addTag)="createTag($event)" (addTag)="createTag($event)"
(addDocumentType)="createDocumentType($event)" (addDocumentType)="createDocumentType($event)"
(addCorrespondent)="createCorrespondent($event)"> (addCorrespondent)="createCorrespondent($event)">
@@ -43,7 +43,7 @@ import {
} from 'src/app/data/filter-rule-type' } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
@@ -1528,6 +1528,113 @@ describe('DocumentDetailComponent', () => {
expect(component.suggestionsLoading()).toBeFalsy() expect(component.suggestionsLoading()).toBeFalsy()
}) })
it('should get and merge ML and AI suggestions when source is both', () => {
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.Both
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42], dates: ['2024-01-01'] }))
const aiSuggestionsSpy = jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(
of({ title: 'AI title', tags: [42, 43], suggested_tags: ['New'] })
)
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
expect(aiSuggestionsSpy).toHaveBeenCalled()
expect(component.suggestions().title).toEqual('AI title')
expect(component.suggestions().tags).toEqual([42, 43])
expect(component.suggestions().suggested_tags).toEqual(['New'])
expect(component.suggestions().dates).toEqual(['2024-01-01'])
})
it('should only fetch sources not yet fetched for the document', () => {
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, false)
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.ML
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42] }))
const aiSuggestionsSpy = jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(of({ tags: [43] }))
initNormally()
component.getSuggestions()
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).not.toHaveBeenCalled()
component.getSuggestions(SuggestionSource.Both)
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).toHaveBeenCalledTimes(1)
expect(component.suggestions().tags).toEqual([42, 43])
component.getSuggestions(SuggestionSource.Both)
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).toHaveBeenCalledTimes(1)
})
it('should use the per-document source override and reset it on document change', () => {
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, false)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
initNormally()
expect(component.suggestionSource).toEqual(SuggestionSource.AI)
component.suggestionSourceOverride.set(SuggestionSource.ML)
expect(component.suggestionSource).toEqual(SuggestionSource.ML)
jest
.spyOn(documentService, 'get')
.mockReturnValueOnce(of(Object.assign({}, doc)))
;(component as any).loadDocument(doc.id, true)
expect(component.suggestionSourceOverride()).toBeNull()
expect(component.fetchedSuggestionSources()).toEqual([])
})
it('should keep suggestions from one source if the other fails', () => {
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.Both
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const errorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42] }))
jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(throwError(() => new Error('failed')))
initNormally()
expect(errorSpy).toHaveBeenCalled()
expect(component.suggestions().tags).toEqual([42])
expect(component.fetchedSuggestionSources()).toEqual([SuggestionSource.ML])
})
it('should show error if needed for get suggestions', () => { it('should show error if needed for get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const errorSpy = jest.spyOn(toastService, 'showError') const errorSpy = jest.spyOn(toastService, 'showError')
@@ -28,7 +28,7 @@ import {
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DeviceDetectorService } from 'ngx-device-detector' import { DeviceDetectorService } from 'ngx-device-detector'
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs' import { BehaviorSubject, merge, Observable, of, Subject, timer } from 'rxjs'
import { import {
catchError, catchError,
debounceTime, debounceTime,
@@ -48,7 +48,10 @@ import { DataType } from 'src/app/data/datatype'
import { Document, DocumentVersionInfo } from 'src/app/data/document' import { Document, DocumentVersionInfo } from 'src/app/data/document'
import { DocumentMetadata } from 'src/app/data/document-metadata' import { DocumentMetadata } from 'src/app/data/document-metadata'
import { DocumentNote } from 'src/app/data/document-note' import { DocumentNote } from 'src/app/data/document-note'
import { DocumentSuggestions } from 'src/app/data/document-suggestions' import {
DocumentSuggestions,
mergeSuggestions,
} from 'src/app/data/document-suggestions'
import { DocumentType } from 'src/app/data/document-type' import { DocumentType } from 'src/app/data/document-type'
import { FilterRule } from 'src/app/data/filter-rule' import { FilterRule } from 'src/app/data/filter-rule'
import { import {
@@ -63,7 +66,7 @@ import {
import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithId } from 'src/app/data/object-with-id'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -240,6 +243,10 @@ export class DocumentDetailComponent
private readonly autoSuggestSetting = this.settings.getSignal<boolean>( private readonly autoSuggestSetting = this.settings.getSignal<boolean>(
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST
) )
private readonly suggestionSourceSetting =
this.settings.getSignal<SuggestionSource>(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE
)
private readonly hiddenFieldsSetting = this.settings.getSignal< private readonly hiddenFieldsSetting = this.settings.getSignal<
DocumentDetailFieldID[] DocumentDetailFieldID[]
>(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS) >(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
@@ -261,6 +268,9 @@ export class DocumentDetailComponent
readonly metadata = signal<DocumentMetadata>(undefined) readonly metadata = signal<DocumentMetadata>(undefined)
readonly suggestions = signal<DocumentSuggestions>(undefined) readonly suggestions = signal<DocumentSuggestions>(undefined)
readonly suggestionsLoading = signal(false) readonly suggestionsLoading = signal(false)
// per-document, resets on navigation
readonly suggestionSourceOverride = signal<SuggestionSource>(null)
readonly fetchedSuggestionSources = signal<SuggestionSource[]>([])
readonly users = signal<User[]>(undefined) readonly users = signal<User[]>(undefined)
readonly title = signal<string>(undefined) readonly title = signal<string>(undefined)
@@ -365,6 +375,15 @@ export class DocumentDetailComponent
return this.autoSuggestSetting() return this.autoSuggestSetting()
} }
get defaultSuggestionSource(): SuggestionSource {
return this.aiEnabled ? this.suggestionSourceSetting() : SuggestionSource.ML
}
get suggestionSource(): SuggestionSource {
if (!this.aiEnabled) return SuggestionSource.ML
return this.suggestionSourceOverride() ?? this.defaultSuggestionSource
}
get archiveContentRenderType(): ContentRenderType { get archiveContentRenderType(): ContentRenderType {
const hasArchiveVersion = const hasArchiveVersion =
this.metadata()?.has_archive_version ?? this.metadata()?.has_archive_version ??
@@ -590,6 +609,8 @@ export class DocumentDetailComponent
} }
this.documentId.set(doc.id) this.documentId.set(doc.id)
this.suggestions.set(null) this.suggestions.set(null)
this.suggestionSourceOverride.set(null)
this.fetchedSuggestionSources.set([])
const openDocument = this.openDocumentService.getOpenDocument( const openDocument = this.openDocumentService.getOpenDocument(
this.documentId() this.documentId()
) )
@@ -1077,29 +1098,44 @@ export class DocumentDetailComponent
return this.documentForm.get('custom_fields') as FormArray return this.documentForm.get('custom_fields') as FormArray
} }
getSuggestions() { getSuggestions(source: SuggestionSource = this.suggestionSource) {
const sources = (
source === SuggestionSource.Both
? [SuggestionSource.ML, SuggestionSource.AI]
: [source]
).filter((s) => !this.fetchedSuggestionSources().includes(s))
if (!sources.length) return
this.suggestionsLoading.set(true) this.suggestionsLoading.set(true)
const suggestionsObservable = this.aiEnabled merge(
? this.documentsService.getAiSuggestions(this.documentId()) ...sources.map((s) =>
: this.documentsService.getSuggestions(this.documentId()) (s === SuggestionSource.AI
suggestionsObservable ? this.documentsService.getAiSuggestions(this.documentId())
: this.documentsService.getSuggestions(this.documentId())
).pipe(
first(),
map((result) => ({ source: s, result })),
catchError((error) => {
this.toastService.showError(
$localize`Error retrieving suggestions.`,
error
)
return of(null)
})
)
)
)
.pipe( .pipe(
first(),
takeUntil(this.unsubscribeNotifier), takeUntil(this.unsubscribeNotifier),
takeUntil(this.docChangeNotifier), takeUntil(this.docChangeNotifier),
finalize(() => this.suggestionsLoading.set(false)) finalize(() => this.suggestionsLoading.set(false))
) )
.subscribe({ .subscribe((response) => {
next: (result) => { if (!response) return
this.suggestions.set(result) this.fetchedSuggestionSources.update((f) => [...f, response.source])
}, this.suggestions.set(
error: (error) => { mergeSuggestions(this.suggestions(), response.result)
this.suggestions.set(null) )
this.toastService.showError(
$localize`Error retrieving suggestions.`,
error
)
},
}) })
} }
@@ -146,6 +146,19 @@ describe('DocumentListComponent', () => {
expect(reloadSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled()
}) })
it('should stop reloading on document deleted after destroy', () => {
const reloadSpy = jest.spyOn(documentListService, 'reload')
const documentDeletedSubject = new Subject<boolean>()
jest
.spyOn(websocketStatusService, 'onDocumentDeleted')
.mockReturnValue(documentDeletedSubject)
fixture.detectChanges()
fixture.destroy()
reloadSpy.mockClear()
documentDeletedSubject.next(true)
expect(reloadSpy).not.toHaveBeenCalled()
})
it('should show score sort fields on fulltext queries', () => { it('should show score sort fields on fulltext queries', () => {
documentListService.setFilterRules([ documentListService.setFilterRules([
{ {
@@ -270,9 +270,12 @@ export class DocumentListComponent
this.list.reload() this.list.reload()
}) })
this.websocketStatusService.onDocumentDeleted().subscribe(() => { this.websocketStatusService
this.list.reload() .onDocumentDeleted()
}) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.list.reload()
})
this.route.paramMap this.route.paramMap
.pipe( .pipe(
@@ -15,3 +15,33 @@ export interface DocumentSuggestions {
dates?: string[] // ISO-formatted date string e.g. 2022-11-03 dates?: string[] // ISO-formatted date string e.g. 2022-11-03
} }
const union = <T>(a: T[] = [], b: T[] = []): T[] => [...new Set([...a, ...b])]
export function mergeSuggestions(
a: DocumentSuggestions,
b: DocumentSuggestions
): DocumentSuggestions {
if (!a) return b
return {
title: a.title || b.title,
tags: union(a.tags, b.tags),
suggested_tags: union(a.suggested_tags, b.suggested_tags),
correspondents: union(a.correspondents, b.correspondents),
suggested_correspondents: union(
a.suggested_correspondents,
b.suggested_correspondents
),
document_types: union(a.document_types, b.document_types),
suggested_document_types: union(
a.suggested_document_types,
b.suggested_document_types
),
storage_paths: union(a.storage_paths, b.storage_paths),
suggested_storage_paths: union(
a.suggested_storage_paths,
b.suggested_storage_paths
),
dates: union(a.dates, b.dates),
}
}
+9
View File
@@ -353,6 +353,14 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_AI_LLM_EMBEDDING_MODEL', config_key: 'PAPERLESS_AI_LLM_EMBEDDING_MODEL',
category: ConfigCategory.AI, category: ConfigCategory.AI,
}, },
{
key: 'llm_embedding_api_key',
title: $localize`LLM Embedding API Key`,
type: ConfigOptionType.Password,
note: $localize`Used for embeddings when set, otherwise LLM API key is used.`,
config_key: 'PAPERLESS_AI_LLM_EMBEDDING_API_KEY',
category: ConfigCategory.AI,
},
{ {
key: 'llm_embedding_endpoint', key: 'llm_embedding_endpoint',
title: $localize`LLM Embedding Endpoint`, title: $localize`LLM Embedding Endpoint`,
@@ -457,6 +465,7 @@ export interface PaperlessConfig extends ObjectWithId {
ai_enabled: boolean ai_enabled: boolean
llm_embedding_backend: string llm_embedding_backend: string
llm_embedding_model: string llm_embedding_model: string
llm_embedding_api_key: string
llm_embedding_endpoint: string llm_embedding_endpoint: string
llm_embedding_chunk_size: number llm_embedding_chunk_size: number
llm_context_size: number llm_context_size: number
+13
View File
@@ -20,6 +20,12 @@ export enum GlobalSearchType {
TITLE_CONTENT = 'title-content', TITLE_CONTENT = 'title-content',
} }
export enum SuggestionSource {
ML = 'ml',
AI = 'ai',
Both = 'both',
}
export enum CollapsibleSection { export enum CollapsibleSection {
ATTRIBUTES = 'attributes', ATTRIBUTES = 'attributes',
} }
@@ -98,6 +104,8 @@ export const SETTINGS_KEYS = {
'general-settings:document-editing:overlay-thumbnail', 'general-settings:document-editing:overlay-thumbnail',
DOCUMENT_EDITING_AUTO_SUGGEST: DOCUMENT_EDITING_AUTO_SUGGEST:
'general-settings:document-editing:auto-suggest', 'general-settings:document-editing:auto-suggest',
DOCUMENT_EDITING_SUGGESTION_SOURCE:
'general-settings:document-editing:suggestion-source',
DOCUMENT_DETAILS_HIDDEN_FIELDS: DOCUMENT_DETAILS_HIDDEN_FIELDS:
'general-settings:document-details:hidden-fields', 'general-settings:document-details:hidden-fields',
SEARCH_DB_ONLY: 'general-settings:search:db-only', SEARCH_DB_ONLY: 'general-settings:search:db-only',
@@ -326,6 +334,11 @@ export const SETTINGS: UiSetting[] = [
type: 'boolean', type: 'boolean',
default: true, default: true,
}, },
{
key: SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
type: 'string',
default: SuggestionSource.AI,
},
{ {
key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS,
type: 'array', type: 'array',
+1 -1
View File
@@ -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.2.0', version: '3.2.1',
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/',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -74,6 +74,7 @@ import {
clipboardCheckFill, clipboardCheckFill,
clipboardFill, clipboardFill,
clockHistory, clockHistory,
cpu,
creditCard, creditCard,
dash, dash,
dashCircle, dashCircle,
@@ -118,6 +119,7 @@ import {
infoCircle, infoCircle,
journalBookmarkFill, journalBookmarkFill,
journals, journals,
lightbulb,
link, link,
list, list,
listNested, listNested,
@@ -322,6 +324,7 @@ const icons = {
clipboardCheckFill, clipboardCheckFill,
clipboardFill, clipboardFill,
clockHistory, clockHistory,
cpu,
cash, cash,
creditCard, creditCard,
dash, dash,
@@ -367,6 +370,7 @@ const icons = {
infoCircle, infoCircle,
journalBookmarkFill, journalBookmarkFill,
journals, journals,
lightbulb,
link, link,
list, list,
listNested, listNested,
+1
View File
@@ -292,6 +292,7 @@ a.btn-link:active,
a.btn-link:focus-visible, a.btn-link:focus-visible,
.btn-link:hover, .btn-link:hover,
.btn-link:active, .btn-link:active,
.btn-link.show,
.btn-link:focus-visible { .btn-link:focus-visible {
color: var(--pngx-primary-lighten-10) !important; color: var(--pngx-primary-lighten-10) !important;
.primary-light & { .primary-light & {
@@ -25,10 +25,20 @@ export class PDFFindController {
onIsPageVisible?: () => boolean onIsPageVisible?: () => boolean
} }
export const LinkTarget = {
NONE: 0,
SELF: 1,
BLANK: 2,
PARENT: 3,
TOP: 4,
}
export class PDFLinkService { export class PDFLinkService {
private document?: unknown private document?: unknown
private viewer?: unknown private viewer?: unknown
constructor(readonly options: Record<string, unknown> = {}) {}
setDocument(document: unknown): void { setDocument(document: unknown): void {
this.document = document this.document = document
} }
+151
View File
@@ -0,0 +1,151 @@
"""Fixtures available to every Paperless-ngx app.
Loaded automatically for every test path. Keep module-scope imports minimal:
this file is imported for every session, so anything heavy belongs inside
the fixture body that needs it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
if TYPE_CHECKING:
from collections.abc import Generator
from pathlib import Path
from django.contrib.auth.models import User
from pytest_django.fixtures import Settings
from rest_framework.test import APIClient
from paperless_testing.dirs import PaperlessDirs
from paperless_testing.fakes.progress import FakeProgressManager
@pytest.fixture(scope="session", autouse=True)
def faker_session_locale() -> str:
"""Pin Faker's locale so generated data does not follow the host locale.
The seed itself is left to pytest-randomly, which derives one per run.
"""
return "en_US"
@pytest.fixture(autouse=True)
def _fast_password_hasher(settings: Settings) -> None:
"""Hash test passwords with MD5 instead of Django's default PBKDF2.
PBKDF2 is deliberately slow, and every ``admin_user`` or
``create_superuser`` call pays for it: about 600 ms each. No test depends
on the hash format, only on ``check_password`` and on the stored value
changing when the password does.
"""
settings.PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
@pytest.fixture(autouse=True)
def _clear_content_type_caches() -> None:
"""Clear Django's ContentType cache and guardian's lru_cache before each test.
Tests that delete and reinsert ContentType/Permission rows (e.g. the
importer) corrupt both caches. Without this fixture a subsequent test on
the same xdist worker sees stale ContentType objects and guardian raises
MixedContentTypeError.
"""
from django.contrib.contenttypes.models import ContentType
from guardian.shortcuts import clear_ct_cache
ContentType.objects.clear_cache()
clear_ct_cache()
@pytest.fixture(autouse=True)
def _clear_django_caches() -> None:
"""Clear every configured cache before each test.
Cached values outlive the test that wrote them: the classifier keys its
vectorized content on a hash of the content itself, so a second test
generating the same fixture data takes the cache-hit path and never calls
the code it is asserting against.
"""
from django.core.cache import caches
for cache in caches.all(initialized_only=False):
cache.clear()
@pytest.fixture
def paperless_dirs(
tmp_path: Path,
settings: Settings,
) -> Generator[PaperlessDirs, None, None]:
"""The standard temp directory layout, applied to Django settings."""
from documents.search import reset_backend
from paperless_testing.dirs import build_paperless_dirs
from paperless_testing.dirs import dirs_settings
dirs = build_paperless_dirs(tmp_path)
for name, value in dirs_settings(dirs).items():
setattr(settings, name, value)
# Not directory settings, but they are needed alongside the layout by the
# sanity checker tests.
settings.IGNORABLE_FILES = {".DS_Store", "Thumbs.db", "desktop.ini"}
settings.APP_LOGO = ""
reset_backend()
yield dirs
reset_backend()
@pytest.fixture
def rest_api_client() -> APIClient:
"""The basic DRF APIClient, unauthenticated."""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def regular_user(db: None) -> User:
"""Unprivileged user for permission boundary tests."""
from paperless_testing.factories import UserFactory
return UserFactory(username="regular")
@pytest.fixture
def admin_client(rest_api_client: APIClient, admin_user: User) -> APIClient:
"""Admin client pre-authenticated and sending the v10 Accept header."""
rest_api_client.force_authenticate(user=admin_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=10")
return rest_api_client
@pytest.fixture
def v9_client(rest_api_client: APIClient, admin_user: User) -> APIClient:
"""Admin client pre-authenticated and sending the v9 Accept header."""
rest_api_client.force_authenticate(user=admin_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=9")
return rest_api_client
@pytest.fixture
def user_client(rest_api_client: APIClient, regular_user: User) -> APIClient:
"""Regular-user client pre-authenticated and sending the v10 Accept header."""
rest_api_client.force_authenticate(user=regular_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=10")
return rest_api_client
@pytest.fixture
def fake_progress_manager(
monkeypatch: pytest.MonkeyPatch,
) -> type[FakeProgressManager]:
"""Replace documents.tasks.ProgressManager with the fake, so consuming a file
in a test never tries to reach a broker."""
from paperless_testing.fakes.progress import FakeProgressManager
monkeypatch.setattr("documents.tasks.ProgressManager", FakeProgressManager)
return FakeProgressManager
-1
View File
@@ -26,7 +26,6 @@ class DocumentsConfig(AppConfig):
document_consumption_finished.connect(set_document_type) document_consumption_finished.connect(set_document_type)
document_consumption_finished.connect(set_tags) document_consumption_finished.connect(set_tags)
document_consumption_finished.connect(set_storage_path) document_consumption_finished.connect(set_storage_path)
document_consumption_finished.connect(add_to_index)
document_consumption_finished.connect(run_workflows_added) document_consumption_finished.connect(run_workflows_added)
document_consumption_finished.connect(add_to_index) document_consumption_finished.connect(add_to_index)
document_consumption_finished.connect(add_or_update_document_in_llm_index) document_consumption_finished.connect(add_or_update_document_in_llm_index)
+3 -2
View File
@@ -857,8 +857,9 @@ class ConsumerPlugin(
self.log.debug(f"Creation date from parse_date: {create_date}") self.log.debug(f"Creation date from parse_date: {create_date}")
else: else:
stats = Path(self.input_doc.original_file).stat() stats = Path(self.input_doc.original_file).stat()
create_date = timezone.make_aware( create_date = datetime.datetime.fromtimestamp(
datetime.datetime.fromtimestamp(stats.st_mtime), stats.st_mtime,
tz=timezone.get_current_timezone(),
) )
self.log.debug(f"Creation date from st_mtime: {create_date}") self.log.debug(f"Creation date from st_mtime: {create_date}")

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