mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-26 22:49:01 -06:00
Compare commits
14 Commits
71ecdc528e
...
release/v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
891f4a2faf | ||
|
|
2312314aa7 | ||
|
|
72e8b73108 | ||
|
|
5c9ff367e3 | ||
|
|
94f6b8d36d | ||
|
|
32d04e1fd3 | ||
|
|
56c744fd56 | ||
|
|
d1aa76e4ce | ||
|
|
5381bc5907 | ||
|
|
771f3f150a | ||
|
|
ecfeff5054 | ||
|
|
37477d391e | ||
|
|
2f1cd31e31 | ||
|
|
742c136773 |
1
.github/release-drafter.yml
vendored
1
.github/release-drafter.yml
vendored
@@ -44,6 +44,7 @@ include-labels:
|
|||||||
- 'notable'
|
- 'notable'
|
||||||
exclude-labels:
|
exclude-labels:
|
||||||
- 'skip-changelog'
|
- 'skip-changelog'
|
||||||
|
filter-by-commitish: true
|
||||||
category-template: '### $TITLE'
|
category-template: '### $TITLE'
|
||||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||||
change-title-escapes: '\<*_&#@'
|
change-title-escapes: '\<*_&#@'
|
||||||
|
|||||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -617,6 +617,7 @@ jobs:
|
|||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||||
publish: true # ensures release is not marked as draft
|
publish: true # ensures release is not marked as draft
|
||||||
|
commitish: ${{ github.sha }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Upload release archive
|
- name: Upload release archive
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py management_command "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py management_command "$@"
|
python3 manage.py management_command "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py management_command "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py convert_mariadb_uuid "$@"
|
python3 manage.py convert_mariadb_uuid "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py createsuperuser "$@"
|
python3 manage.py createsuperuser "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py decrypt_documents "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py decrypt_documents "$@"
|
python3 manage.py decrypt_documents "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py decrypt_documents "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_archiver "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_archiver "$@"
|
python3 manage.py document_archiver "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_archiver "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_create_classifier "$@"
|
python3 manage.py document_create_classifier "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_exporter "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_exporter "$@"
|
python3 manage.py document_exporter "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_exporter "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_fuzzy_match "$@"
|
python3 manage.py document_fuzzy_match "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_importer "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_importer "$@"
|
python3 manage.py document_importer "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_importer "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_index "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_index "$@"
|
python3 manage.py document_index "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_index "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_renamer "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_renamer "$@"
|
python3 manage.py document_renamer "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_renamer "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_retagger "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_retagger "$@"
|
python3 manage.py document_retagger "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_retagger "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_sanity_checker "$@"
|
python3 manage.py document_sanity_checker "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py document_thumbnails "$@"
|
python3 manage.py document_thumbnails "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py mail_fetcher "$@"
|
python3 manage.py mail_fetcher "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py manage_superuser "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py manage_superuser "$@"
|
python3 manage.py manage_superuser "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py manage_superuser "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
|
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
|
||||||
python3 manage.py prune_audit_logs "$@"
|
python3 manage.py prune_audit_logs "$@"
|
||||||
else
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
echo "Unknown user."
|
s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,44 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## paperless-ngx 2.20.4
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-28cf-xvcf-hw6m](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-28cf-xvcf-hw6m)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659))
|
|
||||||
- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661))
|
|
||||||
- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666))
|
|
||||||
- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731))
|
|
||||||
- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735))
|
|
||||||
|
|
||||||
### All App Changes
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>5 changes</summary>
|
|
||||||
|
|
||||||
- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659))
|
|
||||||
- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661))
|
|
||||||
- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666))
|
|
||||||
- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731))
|
|
||||||
- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## paperless-ngx 2.20.3
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-7cq3-mhxq-w946](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7cq3-mhxq-w946)
|
|
||||||
|
|
||||||
## paperless-ngx 2.20.2
|
## paperless-ngx 2.20.2
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Resolve [GHSA-6653-vcx4-69mc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-6653-vcx4-69mc)
|
|
||||||
- Resolve [GHSA-24x5-wp64-9fcc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-24x5-wp64-9fcc)
|
|
||||||
|
|
||||||
### Features / Enhancements
|
### Features / Enhancements
|
||||||
|
|
||||||
- Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537))
|
- Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537))
|
||||||
|
|||||||
@@ -170,18 +170,11 @@ Available options are `postgresql` and `mariadb`.
|
|||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
A pool of 8-10 connections per worker is typically sufficient.
|
A small pool is typically sufficient — for example, a size of 4.
|
||||||
If you encounter error messages such as `couldn't get a connection`
|
Make sure your PostgreSQL server's max_connections setting is large enough to handle:
|
||||||
or database connection timeouts, you probably need to increase the pool size.
|
```(Paperless workers + Celery workers) × pool size + safety margin```
|
||||||
|
For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4:
|
||||||
!!! warning
|
(4 + 2) × 4 + 10 = 34 connections required.
|
||||||
Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools:
|
|
||||||
`(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with
|
|
||||||
4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`,
|
|
||||||
so `max_connections = 60` (or even more) is appropriate.
|
|
||||||
|
|
||||||
This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications,
|
|
||||||
you should increase `max_connections` accordingly.
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
||||||
|
|
||||||
@@ -1014,7 +1007,7 @@ still perform some basic text pre-processing before matching.
|
|||||||
|
|
||||||
: See also `PAPERLESS_NLTK_DIR`.
|
: See also `PAPERLESS_NLTK_DIR`.
|
||||||
|
|
||||||
Defaults to true, enabling the feature.
|
Defaults to 1.
|
||||||
|
|
||||||
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
||||||
|
|
||||||
@@ -1081,7 +1074,7 @@ valid crontab(5) expression describing when to run.
|
|||||||
|
|
||||||
: Enables compression of the responses from the webserver.
|
: Enables compression of the responses from the webserver.
|
||||||
|
|
||||||
: Defaults to true, enabling compression.
|
: Defaults to 1, enabling compression.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.20.4"
|
version = "2.20.5"
|
||||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -238,7 +238,7 @@ lint.isort.force-single-line = true
|
|||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
write-changes = true
|
write-changes = true
|
||||||
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober"
|
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish"
|
||||||
skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json"
|
skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "2.20.4",
|
"version": "2.20.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
expect(component.object.actions.length).toEqual(2)
|
expect(component.object.actions.length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update order and remove ids from actions on drag n drop', () => {
|
it('should update order on drag n drop', () => {
|
||||||
const action1 = workflow.actions[0]
|
const action1 = workflow.actions[0]
|
||||||
const action2 = workflow.actions[1]
|
const action2 = workflow.actions[1]
|
||||||
component.object = workflow
|
component.object = workflow
|
||||||
@@ -261,8 +261,6 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
WorkflowAction[]
|
WorkflowAction[]
|
||||||
>)
|
>)
|
||||||
expect(component.object.actions).toEqual([action2, action1])
|
expect(component.object.actions).toEqual([action2, action1])
|
||||||
expect(action1.id).toBeNull()
|
|
||||||
expect(action2.id).toBeNull()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not include auto matching in algorithms', () => {
|
it('should not include auto matching in algorithms', () => {
|
||||||
|
|||||||
@@ -1283,11 +1283,6 @@ export class WorkflowEditDialogComponent
|
|||||||
const actionField = this.actionFields.at(event.previousIndex)
|
const actionField = this.actionFields.at(event.previousIndex)
|
||||||
this.actionFields.removeAt(event.previousIndex)
|
this.actionFields.removeAt(event.previousIndex)
|
||||||
this.actionFields.insert(event.currentIndex, actionField)
|
this.actionFields.insert(event.currentIndex, actionField)
|
||||||
// removing id will effectively re-create the actions in this order
|
|
||||||
this.object.actions.forEach((a) => (a.id = null))
|
|
||||||
this.actionFields.controls.forEach((c) =>
|
|
||||||
c.get('id').setValue(null, { emitEvent: false })
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
|
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
|
||||||
<div class="tag-option-row d-flex align-items-center">
|
<div class="tag-option-row d-flex align-items-center" [class.w-auto]="!getTag(item.id)?.parent">
|
||||||
@if (item.id && tags) {
|
@if (item.id && tags) {
|
||||||
@if (getTag(item.id)?.parent) {
|
@if (getTag(item.id)?.parent) {
|
||||||
<i-bs name="list-nested" class="me-1"></i-bs>
|
<i-bs name="list-nested" class="me-1"></i-bs>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dropdown hierarchy reveal for ng-select options
|
// Dropdown hierarchy reveal for ng-select options
|
||||||
::ng-deep .ng-dropdown-panel .ng-option {
|
:host ::ng-deep .ng-dropdown-panel .ng-option {
|
||||||
overflow-x: scroll;
|
overflow-x: auto !important;
|
||||||
|
|
||||||
.tag-option-row {
|
.tag-option-row {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -41,12 +41,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal,
|
:host ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal,
|
||||||
::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal {
|
:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator,
|
::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator,
|
||||||
::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator {
|
:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,21 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(reloadSpy).toHaveBeenCalled()
|
expect(reloadSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should use the all list length for collection size when provided', fakeAsync(() => {
|
||||||
|
jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce(
|
||||||
|
of({
|
||||||
|
count: 1,
|
||||||
|
all: [1, 2, 3],
|
||||||
|
results: tags.slice(0, 1),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
component.reloadData()
|
||||||
|
tick(100)
|
||||||
|
|
||||||
|
expect(component.collectionSize).toBe(3)
|
||||||
|
}))
|
||||||
|
|
||||||
it('should support quick filter for objects', () => {
|
it('should support quick filter for objects', () => {
|
||||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
const filterButton = fixture.debugElement.queryAll(By.css('button'))[9]
|
const filterButton = fixture.debugElement.queryAll(By.css('button'))[9]
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
tap((c) => {
|
tap((c) => {
|
||||||
this.unfilteredData = c.results
|
this.unfilteredData = c.results
|
||||||
this.data = this.filterData(c.results)
|
this.data = this.filterData(c.results)
|
||||||
this.collectionSize = c.count
|
this.collectionSize = c.all?.length ?? c.count
|
||||||
}),
|
}),
|
||||||
delay(100)
|
delay(100)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const environment = {
|
|||||||
apiVersion: '9', // match src/paperless/settings.py
|
apiVersion: '9', // match src/paperless/settings.py
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
tag: 'prod',
|
tag: 'prod',
|
||||||
version: '2.20.4',
|
version: '2.20.5',
|
||||||
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/',
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class DocumentMetadataOverrides:
|
|||||||
).values_list("id", flat=True),
|
).values_list("id", flat=True),
|
||||||
)
|
)
|
||||||
overrides.custom_fields = {
|
overrides.custom_fields = {
|
||||||
custom_field.id: custom_field.value
|
custom_field.field.id: custom_field.value
|
||||||
for custom_field in doc.custom_fields.all()
|
for custom_field in doc.custom_fields.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ def rewrite_natural_date_keywords(query_string: str) -> str:
|
|||||||
|
|
||||||
case "this year":
|
case "this year":
|
||||||
start = datetime(local_now.year, 1, 1, 0, 0, 0, tzinfo=tz)
|
start = datetime(local_now.year, 1, 1, 0, 0, 0, tzinfo=tz)
|
||||||
end = datetime.combine(today, time.max, tzinfo=tz)
|
end = datetime(local_now.year, 12, 31, 23, 59, 59, tzinfo=tz)
|
||||||
|
|
||||||
case "previous week":
|
case "previous week":
|
||||||
days_since_monday = local_now.weekday()
|
days_since_monday = local_now.weekday()
|
||||||
|
|||||||
28
src/documents/migrations/1075_workflowaction_order.py
Normal file
28
src/documents/migrations/1075_workflowaction_order.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-14 16:53
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import F
|
||||||
|
|
||||||
|
|
||||||
|
def populate_action_order(apps, schema_editor):
|
||||||
|
WorkflowAction = apps.get_model("documents", "WorkflowAction")
|
||||||
|
WorkflowAction.objects.all().update(order=F("id"))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="order",
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name="order"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
populate_action_order,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1294,6 +1294,8 @@ class WorkflowAction(models.Model):
|
|||||||
default=WorkflowActionType.ASSIGNMENT,
|
default=WorkflowActionType.ASSIGNMENT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
order = models.PositiveIntegerField(_("order"), default=0)
|
||||||
|
|
||||||
assign_title = models.TextField(
|
assign_title = models.TextField(
|
||||||
_("assign title"),
|
_("assign title"),
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@@ -580,6 +580,10 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
def get_children(self, obj):
|
def get_children(self, obj):
|
||||||
|
children_map = self.context.get("children_map")
|
||||||
|
if children_map is not None:
|
||||||
|
children = children_map.get(obj.pk, [])
|
||||||
|
else:
|
||||||
filter_q = self.context.get("document_count_filter")
|
filter_q = self.context.get("document_count_filter")
|
||||||
request = self.context.get("request")
|
request = self.context.get("request")
|
||||||
if filter_q is None:
|
if filter_q is None:
|
||||||
@@ -587,7 +591,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
filter_q = get_document_count_filter_for_user(user)
|
filter_q = get_document_count_filter_for_user(user)
|
||||||
self.context["document_count_filter"] = filter_q
|
self.context["document_count_filter"] = filter_q
|
||||||
|
|
||||||
children_queryset = (
|
children = (
|
||||||
obj.get_children_queryset()
|
obj.get_children_queryset()
|
||||||
.select_related("owner")
|
.select_related("owner")
|
||||||
.annotate(document_count=Count("documents", filter=filter_q))
|
.annotate(document_count=Count("documents", filter=filter_q))
|
||||||
@@ -595,15 +599,15 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
|
|
||||||
view = self.context.get("view")
|
view = self.context.get("view")
|
||||||
ordering = (
|
ordering = (
|
||||||
OrderingFilter().get_ordering(request, children_queryset, view)
|
OrderingFilter().get_ordering(request, children, view)
|
||||||
if request and view
|
if request and view
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
ordering = ordering or (Lower("name"),)
|
ordering = ordering or (Lower("name"),)
|
||||||
children_queryset = children_queryset.order_by(*ordering)
|
children = children.order_by(*ordering)
|
||||||
|
|
||||||
serializer = TagSerializer(
|
serializer = TagSerializer(
|
||||||
children_queryset,
|
children,
|
||||||
many=True,
|
many=True,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
full_perms=self.full_perms,
|
full_perms=self.full_perms,
|
||||||
@@ -2562,7 +2566,8 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
set_triggers.append(trigger_instance)
|
set_triggers.append(trigger_instance)
|
||||||
|
|
||||||
if actions is not None and actions is not serializers.empty:
|
if actions is not None and actions is not serializers.empty:
|
||||||
for action in actions:
|
for index, action in enumerate(actions):
|
||||||
|
action["order"] = index
|
||||||
assign_tags = action.pop("assign_tags", None)
|
assign_tags = action.pop("assign_tags", None)
|
||||||
assign_view_users = action.pop("assign_view_users", None)
|
assign_view_users = action.pop("assign_view_users", None)
|
||||||
assign_view_groups = action.pop("assign_view_groups", None)
|
assign_view_groups = action.pop("assign_view_groups", None)
|
||||||
@@ -2689,6 +2694,16 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
actions = instance.actions.order_by("order", "pk")
|
||||||
|
data["actions"] = WorkflowActionSerializer(
|
||||||
|
actions,
|
||||||
|
many=True,
|
||||||
|
context=self.context,
|
||||||
|
).data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class TrashSerializer(SerializerWithPerms):
|
class TrashSerializer(SerializerWithPerms):
|
||||||
documents = serializers.ListField(
|
documents = serializers.ListField(
|
||||||
|
|||||||
@@ -769,7 +769,7 @@ def run_workflows(
|
|||||||
|
|
||||||
if matching.document_matches_workflow(document, workflow, trigger_type):
|
if matching.document_matches_workflow(document, workflow, trigger_type):
|
||||||
action: WorkflowAction
|
action: WorkflowAction
|
||||||
for action in workflow.actions.all():
|
for action in workflow.actions.order_by("order", "pk"):
|
||||||
message = f"Applying {action} from {workflow}"
|
message = f"Applying {action} from {workflow}"
|
||||||
if not use_overrides:
|
if not use_overrides:
|
||||||
logger.info(message, extra={"group": logging_group})
|
logger.info(message, extra={"group": logging_group})
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class TestRewriteNaturalDateKeywords(SimpleTestCase):
|
|||||||
(
|
(
|
||||||
"added:this year",
|
"added:this year",
|
||||||
datetime(2025, 7, 15, 12, 0, 0, tzinfo=timezone.utc),
|
datetime(2025, 7, 15, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
("added:[20250101", "TO 20250715"),
|
("added:[20250101", "TO 20251231"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"added:previous year",
|
"added:previous year",
|
||||||
|
|||||||
@@ -448,8 +448,47 @@ class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
context["document_count_filter"] = self.get_document_count_filter()
|
context["document_count_filter"] = self.get_document_count_filter()
|
||||||
|
if hasattr(self, "_children_map"):
|
||||||
|
context["children_map"] = self._children_map
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Build a children map once to avoid per-parent queries in the serializer.
|
||||||
|
"""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
ordering = OrderingFilter().get_ordering(request, queryset, self) or (
|
||||||
|
Lower("name"),
|
||||||
|
)
|
||||||
|
queryset = queryset.order_by(*ordering)
|
||||||
|
|
||||||
|
all_tags = list(queryset)
|
||||||
|
descendant_pks = {pk for tag in all_tags for pk in tag.get_descendants_pks()}
|
||||||
|
|
||||||
|
if descendant_pks:
|
||||||
|
filter_q = self.get_document_count_filter()
|
||||||
|
children_source = list(
|
||||||
|
Tag.objects.filter(pk__in=descendant_pks | {t.pk for t in all_tags})
|
||||||
|
.select_related("owner")
|
||||||
|
.annotate(document_count=Count("documents", filter=filter_q))
|
||||||
|
.order_by(*ordering),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
children_source = all_tags
|
||||||
|
|
||||||
|
children_map = {}
|
||||||
|
for tag in children_source:
|
||||||
|
children_map.setdefault(tag.tn_parent_id, []).append(tag)
|
||||||
|
self._children_map = children_map
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
response = self.get_paginated_response(serializer.data)
|
||||||
|
if descendant_pks:
|
||||||
|
# Include children in the "all" field, if needed
|
||||||
|
response.data["all"] = [tag.pk for tag in children_source]
|
||||||
|
return response
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
old_parent = self.get_object().get_parent()
|
old_parent = self.get_object().get_parent()
|
||||||
tag = serializer.save()
|
tag = serializer.save()
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ def get_workflows_for_trigger(
|
|||||||
wrap it in a list; otherwise fetch enabled workflows for the trigger with
|
wrap it in a list; otherwise fetch enabled workflows for the trigger with
|
||||||
the prefetches used by the runner.
|
the prefetches used by the runner.
|
||||||
"""
|
"""
|
||||||
if workflow_to_run is not None:
|
|
||||||
return [workflow_to_run]
|
|
||||||
|
|
||||||
annotated_actions = (
|
annotated_actions = (
|
||||||
WorkflowAction.objects.select_related(
|
WorkflowAction.objects.select_related(
|
||||||
"assign_correspondent",
|
"assign_correspondent",
|
||||||
@@ -105,10 +102,25 @@ def get_workflows_for_trigger(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
action_prefetch = Prefetch(
|
||||||
|
"actions",
|
||||||
|
queryset=annotated_actions.order_by("order", "pk"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if workflow_to_run is not None:
|
||||||
|
return (
|
||||||
|
Workflow.objects.filter(pk=workflow_to_run.pk)
|
||||||
|
.prefetch_related(
|
||||||
|
action_prefetch,
|
||||||
|
"triggers",
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Workflow.objects.filter(enabled=True, triggers__type=trigger_type)
|
Workflow.objects.filter(enabled=True, triggers__type=trigger_type)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch("actions", queryset=annotated_actions),
|
action_prefetch,
|
||||||
"triggers",
|
"triggers",
|
||||||
)
|
)
|
||||||
.order_by("order")
|
.order_by("order")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[tuple[int, int, int]] = (2, 20, 4)
|
__version__: Final[tuple[int, int, int]] = (2, 20, 5)
|
||||||
# Version string like X.Y.Z
|
# Version string like X.Y.Z
|
||||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||||
# Version string like X.Y
|
# Version string like X.Y
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -2115,7 +2115,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.20.4"
|
version = "2.20.5"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
|||||||
Reference in New Issue
Block a user