mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-27 11:50:31 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8849aff5d9 | ||
|
|
f502cd5e34 | ||
|
|
99ce6b5db3 | ||
|
|
2d955e9697 | ||
|
|
f440e8d33c | ||
|
|
4c264651e8 | ||
|
|
48d97b78bb | ||
|
|
659a0cb2ef | ||
|
|
d02d1e1711 | ||
|
|
3b41810e7b | ||
|
|
12314fcaa8 | ||
|
|
8705bd510a | ||
|
|
7575d60782 | ||
|
|
8b02a23b86 | ||
|
|
87d157096e | ||
|
|
54e332d259 | ||
|
|
506afb4200 | ||
|
|
22a8a4f4ad | ||
|
|
d5425a893d | ||
|
|
cc25144dbf |
@@ -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
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ repos:
|
|||||||
- '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.7
|
||||||
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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -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,6 +12,7 @@ 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
|
||||||
@@ -55,7 +58,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",
|
||||||
@@ -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",
|
||||||
@@ -271,9 +274,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"
|
||||||
@@ -334,13 +337,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:",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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",
|
||||||
|
|||||||
@@ -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/',
|
||||||
|
|||||||
@@ -2855,7 +2855,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
<context context-type="linenumber">104</context>
|
<context context-type="linenumber">104</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Apply AI Suggestions</target>
|
<target state="translated">KI-Vorschläge anwenden</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9172233176401579786" datatype="html" approved="yes">
|
<trans-unit id="9172233176401579786" datatype="html" approved="yes">
|
||||||
<source>Scheduled</source>
|
<source>Scheduled</source>
|
||||||
@@ -3907,7 +3907,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">2,3</context>
|
<context context-type="linenumber">2,3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links</target>
|
<target state="translated">Share Links</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html" approved="yes">
|
<trans-unit id="7844706011418789951" datatype="html" approved="yes">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
@@ -5835,7 +5835,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">182</context>
|
<context context-type="linenumber">182</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error retrieving correspondents</target>
|
<target state="translated">Fehler beim Abrufen der Korrespondenten</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3407606224361082860" datatype="html">
|
<trans-unit id="3407606224361082860" datatype="html">
|
||||||
<source>Error retrieving document types</source>
|
<source>Error retrieving document types</source>
|
||||||
@@ -9623,7 +9623,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1051</context>
|
<context context-type="linenumber">1051</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">You have unsaved changes to the content of this version.</target>
|
<target state="translated">Sie haben ungespeicherte Änderungen am Inhalt dieser Version.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="85184271222513014" datatype="html">
|
<trans-unit id="85184271222513014" datatype="html">
|
||||||
<source>Switching versions will discard them.</source>
|
<source>Switching versions will discard them.</source>
|
||||||
@@ -12574,7 +12574,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">4,5</context>
|
<context context-type="linenumber">4,5</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Manage public links to individual documents and document bundles.</target>
|
<target state="translated">Verwalten Sie öffentliche Links zu einzelnen Dokumenten und Dokumentenbündeln.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740311348735113099" datatype="html">
|
<trans-unit id="3740311348735113099" datatype="html">
|
||||||
<source>Document links</source>
|
<source>Document links</source>
|
||||||
|
|||||||
@@ -3907,7 +3907,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">2,3</context>
|
<context context-type="linenumber">2,3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links</target>
|
<target state="translated">Condividi link</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html">
|
<trans-unit id="7844706011418789951" datatype="html">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
@@ -12411,7 +12411,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">40,41</context>
|
<context context-type="linenumber">40,41</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Expired</target>
|
<target state="translated">Scaduto</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2057574872309338088" datatype="html">
|
<trans-unit id="2057574872309338088" datatype="html">
|
||||||
<source>Copy share link</source>
|
<source>Copy share link</source>
|
||||||
@@ -12455,7 +12455,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
||||||
<context context-type="linenumber">164,165</context>
|
<context context-type="linenumber">164,165</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share link bundles pagination</target>
|
<target state="translated">Paginazione dei pacchetti di link di condivisione</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1541995860059283227" datatype="html">
|
<trans-unit id="1541995860059283227" datatype="html">
|
||||||
<source>Failed to load share link bundles.</source>
|
<source>Failed to load share link bundles.</source>
|
||||||
@@ -12511,7 +12511,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">7,8</context>
|
<context context-type="linenumber">7,8</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation"> No document share links currently exist. </target>
|
<target state="translated"> Attualmente non sono presenti link per la condivisione dei documenti. </target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="894965666917873756" datatype="html">
|
<trans-unit id="894965666917873756" datatype="html">
|
||||||
<source>Document</source>
|
<source>Document</source>
|
||||||
@@ -12519,7 +12519,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">15,16</context>
|
<context context-type="linenumber">15,16</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Document</target>
|
<target state="translated">Documento</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7420713064779772535" datatype="html">
|
<trans-unit id="7420713064779772535" datatype="html">
|
||||||
<source>Delete share link</source>
|
<source>Delete share link</source>
|
||||||
@@ -12527,7 +12527,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">75,76</context>
|
<context context-type="linenumber">75,76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Delete share link</target>
|
<target state="translated">Elimina link di condivisione</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5198271726831144068" datatype="html">
|
<trans-unit id="5198271726831144068" datatype="html">
|
||||||
<source>Share links pagination</source>
|
<source>Share links pagination</source>
|
||||||
@@ -12535,7 +12535,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">104,105</context>
|
<context context-type="linenumber">104,105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links pagination</target>
|
<target state="translated">Paginazione link condivisi</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2976350639944567244" datatype="html">
|
<trans-unit id="2976350639944567244" datatype="html">
|
||||||
<source>Failed to load share links.</source>
|
<source>Failed to load share links.</source>
|
||||||
@@ -12543,7 +12543,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Failed to load share links.</target>
|
<target state="translated">Impossibile caricare i link di condivisione.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4100863040928608880" datatype="html">
|
<trans-unit id="4100863040928608880" datatype="html">
|
||||||
<source>Error retrieving share links.</source>
|
<source>Error retrieving share links.</source>
|
||||||
@@ -12551,7 +12551,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">107</context>
|
<context context-type="linenumber">107</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error retrieving share links.</target>
|
<target state="translated">Errore nel recuperare i link di condivisione.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="326120283142363299" datatype="html">
|
<trans-unit id="326120283142363299" datatype="html">
|
||||||
<source>Share link deleted.</source>
|
<source>Share link deleted.</source>
|
||||||
@@ -12559,7 +12559,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">154</context>
|
<context context-type="linenumber">154</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share link deleted.</target>
|
<target state="translated">Link di condivisione eliminato.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4964931417181298145" datatype="html">
|
<trans-unit id="4964931417181298145" datatype="html">
|
||||||
<source>Error deleting share link.</source>
|
<source>Error deleting share link.</source>
|
||||||
@@ -12567,7 +12567,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">159</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error deleting share link.</target>
|
<target state="translated">Errore nell'eliminare il link condivisione.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3439104787814858626" datatype="html">
|
<trans-unit id="3439104787814858626" datatype="html">
|
||||||
<source>Manage public links to individual documents and document bundles.</source>
|
<source>Manage public links to individual documents and document bundles.</source>
|
||||||
@@ -12575,7 +12575,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">4,5</context>
|
<context context-type="linenumber">4,5</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Manage public links to individual documents and document bundles.</target>
|
<target state="translated">Gestisci i link pubblici ai singoli documenti e pacchetti di documenti.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740311348735113099" datatype="html">
|
<trans-unit id="3740311348735113099" datatype="html">
|
||||||
<source>Document links</source>
|
<source>Document links</source>
|
||||||
@@ -12583,7 +12583,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">18,19</context>
|
<context context-type="linenumber">18,19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Document links</target>
|
<target state="translated">Link al documento</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8216780576869093031" datatype="html">
|
<trans-unit id="8216780576869093031" datatype="html">
|
||||||
<source>Bundles</source>
|
<source>Bundles</source>
|
||||||
@@ -12591,7 +12591,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">26,27</context>
|
<context context-type="linenumber">26,27</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Bundles</target>
|
<target state="translated">Pacchetti</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1229748338333965418" datatype="html">
|
<trans-unit id="1229748338333965418" datatype="html">
|
||||||
<source>Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow.</source>
|
<source>Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow.</source>
|
||||||
|
|||||||
@@ -3907,7 +3907,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">2,3</context>
|
<context context-type="linenumber">2,3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links</target>
|
<target state="translated">Ссылки для обмена</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html">
|
<trans-unit id="7844706011418789951" datatype="html">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
@@ -12410,7 +12410,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">40,41</context>
|
<context context-type="linenumber">40,41</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Expired</target>
|
<target state="translated">Истёкшие</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2057574872309338088" datatype="html">
|
<trans-unit id="2057574872309338088" datatype="html">
|
||||||
<source>Copy share link</source>
|
<source>Copy share link</source>
|
||||||
@@ -12454,7 +12454,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
||||||
<context context-type="linenumber">164,165</context>
|
<context context-type="linenumber">164,165</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share link bundles pagination</target>
|
<target state="translated">Пагинация ссылок</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1541995860059283227" datatype="html">
|
<trans-unit id="1541995860059283227" datatype="html">
|
||||||
<source>Failed to load share link bundles.</source>
|
<source>Failed to load share link bundles.</source>
|
||||||
@@ -12510,7 +12510,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">7,8</context>
|
<context context-type="linenumber">7,8</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation"> No document share links currently exist. </target>
|
<target state="translated"> В настоящее время нет общих ссылок. </target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="894965666917873756" datatype="html">
|
<trans-unit id="894965666917873756" datatype="html">
|
||||||
<source>Document</source>
|
<source>Document</source>
|
||||||
@@ -12518,7 +12518,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">15,16</context>
|
<context context-type="linenumber">15,16</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Document</target>
|
<target state="translated">Документ</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7420713064779772535" datatype="html">
|
<trans-unit id="7420713064779772535" datatype="html">
|
||||||
<source>Delete share link</source>
|
<source>Delete share link</source>
|
||||||
@@ -12526,7 +12526,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">75,76</context>
|
<context context-type="linenumber">75,76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Delete share link</target>
|
<target state="translated">Удалить ссылку для обмена</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5198271726831144068" datatype="html">
|
<trans-unit id="5198271726831144068" datatype="html">
|
||||||
<source>Share links pagination</source>
|
<source>Share links pagination</source>
|
||||||
@@ -12534,7 +12534,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">104,105</context>
|
<context context-type="linenumber">104,105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links pagination</target>
|
<target state="translated">Пагинация ссылок</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2976350639944567244" datatype="html">
|
<trans-unit id="2976350639944567244" datatype="html">
|
||||||
<source>Failed to load share links.</source>
|
<source>Failed to load share links.</source>
|
||||||
@@ -12542,7 +12542,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Failed to load share links.</target>
|
<target state="translated">Не удалось загрузить ссылки.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4100863040928608880" datatype="html">
|
<trans-unit id="4100863040928608880" datatype="html">
|
||||||
<source>Error retrieving share links.</source>
|
<source>Error retrieving share links.</source>
|
||||||
@@ -12550,7 +12550,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">107</context>
|
<context context-type="linenumber">107</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error retrieving share links.</target>
|
<target state="translated">Ошибка при получении ссылок.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="326120283142363299" datatype="html">
|
<trans-unit id="326120283142363299" datatype="html">
|
||||||
<source>Share link deleted.</source>
|
<source>Share link deleted.</source>
|
||||||
@@ -12558,7 +12558,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">154</context>
|
<context context-type="linenumber">154</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share link deleted.</target>
|
<target state="translated">Ссылка для обмена удалена.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4964931417181298145" datatype="html">
|
<trans-unit id="4964931417181298145" datatype="html">
|
||||||
<source>Error deleting share link.</source>
|
<source>Error deleting share link.</source>
|
||||||
@@ -12566,7 +12566,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">159</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error deleting share link.</target>
|
<target state="translated">Ошибка при удалении ссылки.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3439104787814858626" datatype="html">
|
<trans-unit id="3439104787814858626" datatype="html">
|
||||||
<source>Manage public links to individual documents and document bundles.</source>
|
<source>Manage public links to individual documents and document bundles.</source>
|
||||||
@@ -12574,7 +12574,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">4,5</context>
|
<context context-type="linenumber">4,5</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Manage public links to individual documents and document bundles.</target>
|
<target state="translated">Управление публичными ссылками на отдельные документы и комплекты документов.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740311348735113099" datatype="html">
|
<trans-unit id="3740311348735113099" datatype="html">
|
||||||
<source>Document links</source>
|
<source>Document links</source>
|
||||||
@@ -12582,7 +12582,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">18,19</context>
|
<context context-type="linenumber">18,19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Document links</target>
|
<target state="translated">Ссылки на документы</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8216780576869093031" datatype="html">
|
<trans-unit id="8216780576869093031" datatype="html">
|
||||||
<source>Bundles</source>
|
<source>Bundles</source>
|
||||||
@@ -12590,7 +12590,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">26,27</context>
|
<context context-type="linenumber">26,27</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Bundles</target>
|
<target state="translated">Комплекты</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1229748338333965418" datatype="html">
|
<trans-unit id="1229748338333965418" datatype="html">
|
||||||
<source>Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow.</source>
|
<source>Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow.</source>
|
||||||
|
|||||||
@@ -2855,7 +2855,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
<context context-type="linenumber">104</context>
|
<context context-type="linenumber">104</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Apply AI Suggestions</target>
|
<target state="translated">Uporabi predloge umetne inteligence</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9172233176401579786" datatype="html">
|
<trans-unit id="9172233176401579786" datatype="html">
|
||||||
<source>Scheduled</source>
|
<source>Scheduled</source>
|
||||||
@@ -3907,7 +3907,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">2,3</context>
|
<context context-type="linenumber">2,3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links</target>
|
<target state="translated">Delite povezave</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html">
|
<trans-unit id="7844706011418789951" datatype="html">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
@@ -12411,7 +12411,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">40,41</context>
|
<context context-type="linenumber">40,41</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Expired</target>
|
<target state="translated">Poteklo</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2057574872309338088" datatype="html">
|
<trans-unit id="2057574872309338088" datatype="html">
|
||||||
<source>Copy share link</source>
|
<source>Copy share link</source>
|
||||||
@@ -12455,7 +12455,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
||||||
<context context-type="linenumber">164,165</context>
|
<context context-type="linenumber">164,165</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share link bundles pagination</target>
|
<target state="translated">Deljenje povezav – strani v sklopih</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1541995860059283227" datatype="html">
|
<trans-unit id="1541995860059283227" datatype="html">
|
||||||
<source>Failed to load share link bundles.</source>
|
<source>Failed to load share link bundles.</source>
|
||||||
@@ -12511,7 +12511,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">7,8</context>
|
<context context-type="linenumber">7,8</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation"> No document share links currently exist. </target>
|
<target state="translated"> Trenutno ni povezav za skupno rabo dokumentov. </target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="894965666917873756" datatype="html">
|
<trans-unit id="894965666917873756" datatype="html">
|
||||||
<source>Document</source>
|
<source>Document</source>
|
||||||
@@ -12519,7 +12519,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">15,16</context>
|
<context context-type="linenumber">15,16</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Document</target>
|
<target state="translated">Dokument</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7420713064779772535" datatype="html">
|
<trans-unit id="7420713064779772535" datatype="html">
|
||||||
<source>Delete share link</source>
|
<source>Delete share link</source>
|
||||||
@@ -12527,7 +12527,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">75,76</context>
|
<context context-type="linenumber">75,76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Delete share link</target>
|
<target state="translated">Izbriši povezavo za deljenje</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5198271726831144068" datatype="html">
|
<trans-unit id="5198271726831144068" datatype="html">
|
||||||
<source>Share links pagination</source>
|
<source>Share links pagination</source>
|
||||||
@@ -12535,7 +12535,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.html</context>
|
||||||
<context context-type="linenumber">104,105</context>
|
<context context-type="linenumber">104,105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share links pagination</target>
|
<target state="translated">Deljenje povezav – strani v sklopih</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2976350639944567244" datatype="html">
|
<trans-unit id="2976350639944567244" datatype="html">
|
||||||
<source>Failed to load share links.</source>
|
<source>Failed to load share links.</source>
|
||||||
@@ -12543,7 +12543,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Failed to load share links.</target>
|
<target state="translated">Povezav za deljenje ni bilo mogoče naložiti.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4100863040928608880" datatype="html">
|
<trans-unit id="4100863040928608880" datatype="html">
|
||||||
<source>Error retrieving share links.</source>
|
<source>Error retrieving share links.</source>
|
||||||
@@ -12551,7 +12551,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">107</context>
|
<context context-type="linenumber">107</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error retrieving share links.</target>
|
<target state="translated">Napaka pri pridobivanju povezav za deljenje.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="326120283142363299" datatype="html">
|
<trans-unit id="326120283142363299" datatype="html">
|
||||||
<source>Share link deleted.</source>
|
<source>Share link deleted.</source>
|
||||||
@@ -12559,7 +12559,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">154</context>
|
<context context-type="linenumber">154</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Share link deleted.</target>
|
<target state="translated">Povezava za deljenje je bila izbrisana.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4964931417181298145" datatype="html">
|
<trans-unit id="4964931417181298145" datatype="html">
|
||||||
<source>Error deleting share link.</source>
|
<source>Error deleting share link.</source>
|
||||||
@@ -12567,7 +12567,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-list/share-link-list.component.ts</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">159</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Error deleting share link.</target>
|
<target state="translated">Napaka pri brisanju povezave za deljenje.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3439104787814858626" datatype="html">
|
<trans-unit id="3439104787814858626" datatype="html">
|
||||||
<source>Manage public links to individual documents and document bundles.</source>
|
<source>Manage public links to individual documents and document bundles.</source>
|
||||||
@@ -12575,7 +12575,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">4,5</context>
|
<context context-type="linenumber">4,5</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Manage public links to individual documents and document bundles.</target>
|
<target state="translated">Upravljajte javne povezave do posameznih dokumentov in paketov dokumentov.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740311348735113099" datatype="html">
|
<trans-unit id="3740311348735113099" datatype="html">
|
||||||
<source>Document links</source>
|
<source>Document links</source>
|
||||||
@@ -12583,7 +12583,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">18,19</context>
|
<context context-type="linenumber">18,19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Document links</target>
|
<target state="translated">Povezave do dokumentov</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8216780576869093031" datatype="html">
|
<trans-unit id="8216780576869093031" datatype="html">
|
||||||
<source>Bundles</source>
|
<source>Bundles</source>
|
||||||
@@ -12591,7 +12591,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-links.component.html</context>
|
||||||
<context context-type="linenumber">26,27</context>
|
<context context-type="linenumber">26,27</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation">Bundles</target>
|
<target state="translated">Paketi</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1229748338333965418" datatype="html">
|
<trans-unit id="1229748338333965418" datatype="html">
|
||||||
<source>Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow.</source>
|
<source>Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow.</source>
|
||||||
|
|||||||
@@ -11518,7 +11518,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/custom-fields/custom-fields.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/custom-fields/custom-fields.component.html</context>
|
||||||
<context context-type="linenumber">70,72</context>
|
<context context-type="linenumber">70,72</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="translated"/>
|
<target state="translated">Inga fält har definierats.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3032792139967609806" datatype="html">
|
<trans-unit id="3032792139967609806" datatype="html">
|
||||||
<source>Confirm delete field</source>
|
<source>Confirm delete field</source>
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
@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 _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
|
||||||
@@ -337,4 +337,13 @@ def open_or_rebuild_index(index_dir: Path | None = None) -> tantivy.Index:
|
|||||||
idx = tantivy.Index(build_schema(), path=str(index_dir))
|
idx = tantivy.Index(build_schema(), path=str(index_dir))
|
||||||
_write_sentinels(index_dir)
|
_write_sentinels(index_dir)
|
||||||
return idx
|
return idx
|
||||||
return tantivy.Index.open(str(index_dir))
|
try:
|
||||||
|
return tantivy.Index.open(str(index_dir))
|
||||||
|
except ValueError:
|
||||||
|
logger.exception(
|
||||||
|
"Search index is corrupted or incomplete - rebuilding from scratch.",
|
||||||
|
)
|
||||||
|
wipe_index(index_dir)
|
||||||
|
idx = tantivy.Index(build_schema(), path=str(index_dir))
|
||||||
|
_write_sentinels(index_dir)
|
||||||
|
return idx
|
||||||
|
|||||||
@@ -1,34 +1,17 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import zoneinfo
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import filelock
|
import filelock
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from guardian.shortcuts import clear_ct_cache
|
|
||||||
from pytest_django.fixtures import Settings
|
from pytest_django.fixtures import Settings
|
||||||
from rest_framework.test import APIClient
|
|
||||||
|
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
UserModelT = get_user_model()
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class PaperlessDirs:
|
|
||||||
"""Standard Paperless-ngx directory layout for tests."""
|
|
||||||
|
|
||||||
media: Path
|
|
||||||
originals: Path
|
|
||||||
archive: Path
|
|
||||||
thumbnails: Path
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -37,52 +20,24 @@ def samples_dir() -> Path:
|
|||||||
return Path(__file__).parent / "samples" / "documents"
|
return Path(__file__).parent / "samples" / "documents"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def paperless_dirs(tmp_path: Path) -> PaperlessDirs:
|
|
||||||
"""Create and return the directory structure for testing."""
|
|
||||||
media = tmp_path / "media"
|
|
||||||
dirs = PaperlessDirs(
|
|
||||||
media=media,
|
|
||||||
originals=media / "documents" / "originals",
|
|
||||||
archive=media / "documents" / "archive",
|
|
||||||
thumbnails=media / "documents" / "thumbnails",
|
|
||||||
)
|
|
||||||
for d in (dirs.originals, dirs.archive, dirs.thumbnails):
|
|
||||||
d.mkdir(parents=True)
|
|
||||||
return dirs
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def _media_settings(paperless_dirs: PaperlessDirs, settings) -> None:
|
|
||||||
"""Configure Django settings to point at temp directories."""
|
|
||||||
settings.MEDIA_ROOT = paperless_dirs.media
|
|
||||||
settings.ORIGINALS_DIR = paperless_dirs.originals
|
|
||||||
settings.ARCHIVE_DIR = paperless_dirs.archive
|
|
||||||
settings.THUMBNAIL_DIR = paperless_dirs.thumbnails
|
|
||||||
settings.MEDIA_LOCK = paperless_dirs.media / "media.lock"
|
|
||||||
settings.IGNORABLE_FILES = {".DS_Store", "Thumbs.db", "desktop.ini"}
|
|
||||||
settings.APP_LOGO = ""
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def sample_doc(
|
def sample_doc(
|
||||||
paperless_dirs: PaperlessDirs,
|
paperless_dirs: "PaperlessDirs",
|
||||||
_media_settings: None,
|
|
||||||
samples_dir: Path,
|
samples_dir: Path,
|
||||||
) -> "Document":
|
) -> "Document":
|
||||||
"""Create a document with valid files and matching checksums."""
|
"""Create a document with valid files and matching checksums."""
|
||||||
with filelock.FileLock(paperless_dirs.media / "media.lock"):
|
with filelock.FileLock(paperless_dirs.media_lock):
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
samples_dir / "originals" / "0000001.pdf",
|
samples_dir / "originals" / "0000001.pdf",
|
||||||
paperless_dirs.originals / "0000001.pdf",
|
paperless_dirs.originals_dir / "0000001.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
samples_dir / "archive" / "0000001.pdf",
|
samples_dir / "archive" / "0000001.pdf",
|
||||||
paperless_dirs.archive / "0000001.pdf",
|
paperless_dirs.archive_dir / "0000001.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
samples_dir / "thumbnails" / "0000001.webp",
|
samples_dir / "thumbnails" / "0000001.webp",
|
||||||
paperless_dirs.thumbnails / "0000001.webp",
|
paperless_dirs.thumbnail_dir / "0000001.webp",
|
||||||
)
|
)
|
||||||
|
|
||||||
return DocumentFactory(
|
return DocumentFactory(
|
||||||
@@ -117,73 +72,6 @@ def _search_index(
|
|||||||
reset_backend()
|
reset_backend()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def settings_timezone(settings: Settings) -> zoneinfo.ZoneInfo:
|
|
||||||
return zoneinfo.ZoneInfo(settings.TIME_ZONE)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def rest_api_client():
|
|
||||||
"""
|
|
||||||
The basic DRF ApiClient
|
|
||||||
"""
|
|
||||||
yield APIClient()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def regular_user(django_user_model: type[UserModelT]) -> UserModelT:
|
|
||||||
"""Unprivileged authenticated user for permission boundary tests."""
|
|
||||||
return django_user_model.objects.create_user(username="regular", password="regular")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def admin_client(rest_api_client: APIClient, admin_user: UserModelT) -> 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: UserModelT) -> 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: UserModelT) -> 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(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.
|
|
||||||
"""
|
|
||||||
ContentType.objects.clear_cache()
|
|
||||||
clear_ct_cache()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
|
||||||
def faker_session_locale():
|
|
||||||
"""Set Faker locale for reproducibility."""
|
|
||||||
return "en_US"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
|
||||||
def faker_seed():
|
|
||||||
return 12345
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def indexed_document(_search_index: None) -> "Document":
|
def indexed_document(_search_index: None) -> "Document":
|
||||||
"""One searchable document, for tests about what the search endpoint
|
"""One searchable document, for tests about what the search endpoint
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ from rich.console import Console
|
|||||||
|
|
||||||
from documents.management.commands.document_sanity_checker import Command
|
from documents.management.commands.document_sanity_checker import Command
|
||||||
from documents.sanity_checker import SanityCheckMessages
|
from documents.sanity_checker import SanityCheckMessages
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.conftest import PaperlessDirs
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
|
|
||||||
def _render_to_string(messages: SanityCheckMessages) -> str:
|
def _render_to_string(messages: SanityCheckMessages) -> str:
|
||||||
@@ -71,7 +71,7 @@ class TestRenderResultsWithIssues:
|
|||||||
assert "INFO" in output
|
assert "INFO" in output
|
||||||
assert "No OCR data" in output
|
assert "No OCR data" in output
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
@pytest.mark.usefixtures("paperless_dirs")
|
||||||
def test_global_message(self) -> None:
|
def test_global_message(self) -> None:
|
||||||
msgs = SanityCheckMessages()
|
msgs = SanityCheckMessages()
|
||||||
msgs.warning(None, "Orphaned file: /tmp/stray.pdf")
|
msgs.warning(None, "Orphaned file: /tmp/stray.pdf")
|
||||||
@@ -87,7 +87,7 @@ class TestRenderResultsWithIssues:
|
|||||||
assert "Thumbnail missing" in output
|
assert "Thumbnail missing" in output
|
||||||
assert "Checksum mismatch" in output
|
assert "Checksum mismatch" in output
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
@pytest.mark.usefixtures("paperless_dirs")
|
||||||
def test_unknown_doc_pk(self) -> None:
|
def test_unknown_doc_pk(self) -> None:
|
||||||
msgs = SanityCheckMessages()
|
msgs = SanityCheckMessages()
|
||||||
msgs.error(99999, "Ghost document")
|
msgs.error(99999, "Ghost document")
|
||||||
@@ -184,7 +184,6 @@ class TestDocumentSanityCheckerCommand:
|
|||||||
assert "ERROR" in output
|
assert "ERROR" in output
|
||||||
assert "Original of document does not exist" in output
|
assert "Original of document does not exist" in output
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
|
||||||
def test_checksum_mismatch(self, paperless_dirs: PaperlessDirs) -> None:
|
def test_checksum_mismatch(self, paperless_dirs: PaperlessDirs) -> None:
|
||||||
"""Lightweight document with zero-byte files triggers checksum mismatch."""
|
"""Lightweight document with zero-byte files triggers checksum mismatch."""
|
||||||
doc = DocumentFactory(
|
doc = DocumentFactory(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from documents.search._backend import TantivyBackend
|
|||||||
from documents.search._backend import reset_backend
|
from documents.search._backend import reset_backend
|
||||||
from documents.search._schema import build_schema
|
from documents.search._schema import build_schema
|
||||||
from documents.search._tokenizer import register_tokenizers
|
from documents.search._tokenizer import register_tokenizers
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import time_machine
|
import time_machine
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
@@ -22,7 +21,8 @@ from documents.models import DocumentType
|
|||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.search._query import parse_user_query
|
from documents.search._query import parse_user_query
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -148,7 +148,7 @@ class TestJsonSubpaths:
|
|||||||
THEN:
|
THEN:
|
||||||
- Only the document with alice's note matches
|
- Only the document with alice's note matches
|
||||||
"""
|
"""
|
||||||
alice = User.objects.create_user(username="alice")
|
alice = UserFactory(username="alice")
|
||||||
doc_with_note = DocumentFactory(
|
doc_with_note = DocumentFactory(
|
||||||
title="Has note",
|
title="Has note",
|
||||||
content="x",
|
content="x",
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
@@ -19,11 +17,12 @@ from documents.search._backend import WriteBatch
|
|||||||
from documents.search._backend import get_backend
|
from documents.search._backend import get_backend
|
||||||
from documents.search._backend import reset_backend
|
from documents.search._backend import reset_backend
|
||||||
from documents.signals.handlers import add_to_index
|
from documents.signals.handlers import add_to_index
|
||||||
from documents.tests.factories import CorrespondentFactory
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from documents.tests.factories import DocumentTypeFactory
|
from paperless_testing.factories import DocumentTypeFactory
|
||||||
from documents.tests.factories import TagFactory
|
from paperless_testing.factories import TagFactory
|
||||||
from documents.tests.factories import UserFactory
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
pytestmark = [pytest.mark.search, pytest.mark.django_db]
|
pytestmark = [pytest.mark.search, pytest.mark.django_db]
|
||||||
|
|
||||||
@@ -189,7 +188,7 @@ class TestAddOrUpdateIds:
|
|||||||
pk=1,
|
pk=1,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", user, doc)
|
grant_object(user, doc, "view_document")
|
||||||
|
|
||||||
with backend.batch_update() as batch:
|
with backend.batch_update() as batch:
|
||||||
batch.add_or_update_ids([doc.pk])
|
batch.add_or_update_ids([doc.pk])
|
||||||
@@ -209,7 +208,7 @@ class TestAddOrUpdateIds:
|
|||||||
pk=1,
|
pk=1,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", group, doc)
|
grant_object(group, doc, "view_document")
|
||||||
|
|
||||||
with backend.batch_update() as batch:
|
with backend.batch_update() as batch:
|
||||||
batch.add_or_update_ids([doc.pk])
|
batch.add_or_update_ids([doc.pk])
|
||||||
@@ -763,8 +762,8 @@ class TestSearchIds:
|
|||||||
|
|
||||||
def test_respects_permission_filter(self, backend: TantivyBackend) -> None:
|
def test_respects_permission_filter(self, backend: TantivyBackend) -> None:
|
||||||
"""search_ids must respect user permission filtering."""
|
"""search_ids must respect user permission filtering."""
|
||||||
owner = User.objects.create_user("ids_owner")
|
owner = UserFactory(username="ids_owner")
|
||||||
other = User.objects.create_user("ids_other")
|
other = UserFactory(username="ids_other")
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="private doc",
|
title="private doc",
|
||||||
content="secret keyword",
|
content="secret keyword",
|
||||||
@@ -843,7 +842,7 @@ class TestRebuild:
|
|||||||
content="group secret keyword",
|
content="group secret keyword",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", group, doc)
|
grant_object(group, doc, "view_document")
|
||||||
|
|
||||||
backend.rebuild(Document.objects.all())
|
backend.rebuild(Document.objects.all())
|
||||||
|
|
||||||
@@ -1071,7 +1070,7 @@ class TestFieldHandling:
|
|||||||
|
|
||||||
def test_notes_include_user_information(self, backend: TantivyBackend) -> None:
|
def test_notes_include_user_information(self, backend: TantivyBackend) -> None:
|
||||||
"""Notes must be indexed with user information when available for structured queries."""
|
"""Notes must be indexed with user information when available for structured queries."""
|
||||||
user = User.objects.create_user("notewriter")
|
user = UserFactory(username="notewriter")
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="Doc with notes",
|
title="Doc with notes",
|
||||||
content="test",
|
content="test",
|
||||||
@@ -1173,7 +1172,7 @@ class TestHighlightHits:
|
|||||||
notes.note: prefix so the query targets notes content directly, but
|
notes.note: prefix so the query targets notes content directly, but
|
||||||
the snippet is generated from notes_text which stores the same text.
|
the snippet is generated from notes_text which stores the same text.
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user("hl_noteuser")
|
user = UserFactory(username="hl_noteuser")
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="Doc with matching note",
|
title="Doc with matching note",
|
||||||
content="unrelated content",
|
content="unrelated content",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import time_machine
|
|||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.search._errors import InvalidDateQuery
|
from documents.search._errors import InvalidDateQuery
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from documents.search._backend import SearchMode
|
from documents.search._backend import SearchMode
|
||||||
from documents.search._query import parse_simple_text_highlight_query
|
from documents.search._query import parse_simple_text_highlight_query
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -53,7 +53,7 @@ class TestBareJsonFieldPrefixes:
|
|||||||
decoy's content match does not resurface through a demoted
|
decoy's content match does not resurface through a demoted
|
||||||
text search
|
text search
|
||||||
"""
|
"""
|
||||||
alice = User.objects.create_user(username="alice")
|
alice = UserFactory(username="alice")
|
||||||
with_note = DocumentFactory(title="Has note", content="x")
|
with_note = DocumentFactory(title="Has note", content="x")
|
||||||
Note.objects.create(document=with_note, user=alice, note="crocodile")
|
Note.objects.create(document=with_note, user=alice, note="crocodile")
|
||||||
backend.add_or_update(with_note)
|
backend.add_or_update(with_note)
|
||||||
@@ -116,7 +116,7 @@ class TestBareJsonFieldPrefixes:
|
|||||||
document; the default-subpath resolution for the bare
|
document; the default-subpath resolution for the bare
|
||||||
prefix does not interfere with explicit subpath addressing
|
prefix does not interfere with explicit subpath addressing
|
||||||
"""
|
"""
|
||||||
bob = User.objects.create_user(username="bob")
|
bob = UserFactory(username="bob")
|
||||||
doc = DocumentFactory(title="Bob note", content="x")
|
doc = DocumentFactory(title="Bob note", content="x")
|
||||||
Note.objects.create(document=doc, user=bob, note="remark")
|
Note.objects.create(document=doc, user=bob, note="remark")
|
||||||
backend.add_or_update(doc)
|
backend.add_or_update(doc)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import tantivy
|
import tantivy
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from whoosh_compat import FieldKind
|
from whoosh_compat import FieldKind
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
@@ -28,6 +27,7 @@ from documents.models import CustomFieldInstance
|
|||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import Note
|
from documents.models import Note
|
||||||
from documents.search._fields import PUBLIC_FIELDS
|
from documents.search._fields import PUBLIC_FIELDS
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from documents.search._backend import TantivyBackend
|
from documents.search._backend import TantivyBackend
|
||||||
@@ -49,7 +49,7 @@ class TestJsonSubpathsAreWrittenAtIndexTime:
|
|||||||
- Every subpath PUBLIC_FIELDS declares for notes/custom_fields
|
- Every subpath PUBLIC_FIELDS declares for notes/custom_fields
|
||||||
is present as a key in the document's stored JSON payload
|
is present as a key in the document's stored JSON payload
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(username="completeness-user")
|
user = UserFactory(username="completeness-user")
|
||||||
field = CustomField.objects.create(
|
field = CustomField.objects.create(
|
||||||
name="Completeness Field",
|
name="Completeness Field",
|
||||||
data_type=CustomField.FieldDataType.STRING,
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from documents.search._backend import SearchIndexLockError
|
|||||||
from documents.search._backend import TantivyBackend
|
from documents.search._backend import TantivyBackend
|
||||||
from documents.tasks import index_document
|
from documents.tasks import index_document
|
||||||
from documents.tasks import remove_document_from_index
|
from documents.tasks import remove_document_from_index
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pytestmark = pytest.mark.search
|
|||||||
class TestMigrateFulltextQueryFieldPrefixes(TestMigrations):
|
class TestMigrateFulltextQueryFieldPrefixes(TestMigrations):
|
||||||
migrate_from = "0016_sha256_checksums"
|
migrate_from = "0016_sha256_checksums"
|
||||||
migrate_to = "0017_migrate_fulltext_query_field_prefixes"
|
migrate_to = "0017_migrate_fulltext_query_field_prefixes"
|
||||||
|
migrate_once = True
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps) -> None:
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
User = apps.get_model("auth", "User")
|
User = apps.get_model("auth", "User")
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ from typing import TYPE_CHECKING
|
|||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from documents.search._backend import TantivyBackend
|
from documents.search._backend import TantivyBackend
|
||||||
@@ -34,22 +35,22 @@ pytestmark = [pytest.mark.search, pytest.mark.django_db]
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def owner() -> User:
|
def owner() -> User:
|
||||||
return User.objects.create_user(username="owner")
|
return UserFactory(username="owner")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stranger() -> User:
|
def stranger() -> User:
|
||||||
return User.objects.create_user(username="stranger")
|
return UserFactory(username="stranger")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def viewer() -> User:
|
def viewer() -> User:
|
||||||
return User.objects.create_user(username="viewer")
|
return UserFactory(username="viewer")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def group_member() -> User:
|
def group_member() -> User:
|
||||||
user = User.objects.create_user(username="group_member")
|
user = UserFactory(username="group_member")
|
||||||
user.groups.add(Group.objects.create(name="accounting"))
|
user.groups.add(Group.objects.create(name="accounting"))
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ class TestPermissionFilteringOnIndexedDocuments:
|
|||||||
checksum="perm-shared-user",
|
checksum="perm-shared-user",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", viewer, doc)
|
grant_object(viewer, doc, "view_document")
|
||||||
backend.add_or_update(doc)
|
backend.add_or_update(doc)
|
||||||
|
|
||||||
assert backend.search_ids("invoice", user=viewer) == [doc.pk]
|
assert backend.search_ids("invoice", user=viewer) == [doc.pk]
|
||||||
@@ -157,7 +158,7 @@ class TestPermissionFilteringOnIndexedDocuments:
|
|||||||
checksum="perm-shared-group",
|
checksum="perm-shared-group",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", group_member.groups.first(), doc)
|
grant_object(group_member.groups.first(), doc, "view_document")
|
||||||
backend.add_or_update(doc)
|
backend.add_or_update(doc)
|
||||||
|
|
||||||
assert backend.search_ids("invoice", user=group_member) == [doc.pk]
|
assert backend.search_ids("invoice", user=group_member) == [doc.pk]
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ import pytest
|
|||||||
import tantivy
|
import tantivy
|
||||||
from django.conf import settings as django_settings
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
from documents.search._schema import SCHEMA_VERSION
|
||||||
from documents.search._schema import build_schema
|
from documents.search._schema import build_schema
|
||||||
from documents.search._schema import needs_rebuild
|
from documents.search._schema import needs_rebuild
|
||||||
from documents.search._schema import open_or_rebuild_index
|
from documents.search._schema import open_or_rebuild_index
|
||||||
|
from documents.search._schema import schema_fingerprint
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -176,3 +178,50 @@ class TestUpgradeFromReleasedV1Index:
|
|||||||
open_or_rebuild_index(released_v1_index)
|
open_or_rebuild_index(released_v1_index)
|
||||||
|
|
||||||
assert needs_rebuild(released_v1_index) is False
|
assert needs_rebuild(released_v1_index) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCorruptedIndex:
|
||||||
|
@pytest.fixture
|
||||||
|
def current_index_missing_meta_json(self, tmp_path: Path) -> Path:
|
||||||
|
"""A current-schema index directory whose meta.json has gone missing.
|
||||||
|
|
||||||
|
The sentinel matches the current schema, so needs_rebuild() reports
|
||||||
|
False and open_or_rebuild_index() takes the plain Index.open() path.
|
||||||
|
"""
|
||||||
|
index_dir = tmp_path / "index"
|
||||||
|
index_dir.mkdir()
|
||||||
|
tantivy.Index(build_schema(), path=str(index_dir))
|
||||||
|
(index_dir / ".index_settings.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"language": django_settings.SEARCH_LANGUAGE,
|
||||||
|
"schema_fingerprint": schema_fingerprint(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
(index_dir / "meta.json").unlink()
|
||||||
|
return index_dir
|
||||||
|
|
||||||
|
def test_missing_meta_json_is_rebuilt_instead_of_raising(
|
||||||
|
self,
|
||||||
|
current_index_missing_meta_json: Path,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An index directory with a sentinel matching the current
|
||||||
|
schema, but a missing/corrupted meta.json (e.g. an
|
||||||
|
interrupted write left the directory in a torn state)
|
||||||
|
WHEN:
|
||||||
|
- open_or_rebuild_index() is called against it
|
||||||
|
THEN:
|
||||||
|
- It rebuilds the index instead of letting tantivy's
|
||||||
|
ValueError propagate; before this, a corrupted index left
|
||||||
|
every read, write, and `document_index reindex --if-needed`
|
||||||
|
hard-failing indefinitely, since the sentinel already
|
||||||
|
matched and reported the index as up to date
|
||||||
|
"""
|
||||||
|
open_or_rebuild_index(current_index_missing_meta_json)
|
||||||
|
|
||||||
|
assert (current_index_missing_meta_json / "meta.json").exists()
|
||||||
|
tantivy.Index.open(str(current_index_missing_meta_json))
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ from documents.models import Document
|
|||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.search import get_backend
|
from documents.search import get_backend
|
||||||
from documents.search import reset_backend
|
from documents.search import reset_backend
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.factories import TagFactory
|
|
||||||
from documents.tests.factories import UserFactory
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from paperless.admin import PaperlessUserAdmin
|
from paperless.admin import PaperlessUserAdmin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from io import BytesIO
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -11,10 +10,11 @@ from PIL.PngImagePlugin import PngInfo
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import read_streaming_response
|
from documents.tests.utils import read_streaming_response
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
from paperless.models import ColorConvertChoices
|
from paperless.models import ColorConvertChoices
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
||||||
@@ -23,7 +23,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
user = User.objects.create_superuser(username="temp_admin")
|
user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
def test_api_get_config(self) -> None:
|
def test_api_get_config(self) -> None:
|
||||||
@@ -267,7 +267,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- old app_logo file is deleted
|
- old app_logo file is deleted
|
||||||
"""
|
"""
|
||||||
admin = User.objects.create_superuser(username="admin")
|
admin = UserFactory(username="admin", superuser=True)
|
||||||
self.client.force_login(user=admin)
|
self.client.force_login(user=admin)
|
||||||
response = self.client.get("/logo/")
|
response = self.client.get("/logo/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import json
|
|||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -14,9 +12,11 @@ from rest_framework.test import APITestCase
|
|||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
from documents.tests.utils import read_streaming_response
|
from documents.tests.utils import read_streaming_response
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
|
|
||||||
class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
|
class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
|
||||||
@@ -25,7 +25,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
self.doc1 = Document.objects.create(title="unrelated", checksum="A")
|
self.doc1 = Document.objects.create(title="unrelated", checksum="A")
|
||||||
@@ -326,10 +326,8 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_download_insufficient_permissions(self) -> None:
|
def test_download_insufficient_permissions(self) -> None:
|
||||||
user = User.objects.create_user(username="temp_user")
|
user = UserFactory(username="temp_user")
|
||||||
user.user_permissions.add(
|
grant_global(user, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
self.doc2.owner = self.user
|
self.doc2.owner = self.user
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import json
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@@ -15,14 +13,18 @@ from documents.models import Document
|
|||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_all_global
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
user = User.objects.create_superuser(username="temp_admin")
|
user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.user = user
|
self.user = user
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
@@ -284,9 +286,9 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
m,
|
m,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.setup_mock(m, "modify_custom_fields")
|
self.setup_mock(m, "modify_custom_fields")
|
||||||
user = User.objects.create_user(username="doc-owner")
|
user = UserFactory(username="doc-owner")
|
||||||
user.user_permissions.add(Permission.objects.get(codename="change_document"))
|
grant_global(user, "change_document")
|
||||||
other_user = User.objects.create_user(username="other-user")
|
other_user = UserFactory(username="other-user")
|
||||||
source_doc = Document.objects.create(
|
source_doc = Document.objects.create(
|
||||||
checksum="source",
|
checksum="source",
|
||||||
title="Source",
|
title="Source",
|
||||||
@@ -787,10 +789,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
|
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
|
||||||
def test_api_bulk_edit_with_all_true_resolves_owned_duplicates(self, m) -> None:
|
def test_api_bulk_edit_with_all_true_resolves_owned_duplicates(self, m) -> None:
|
||||||
self.setup_mock(m, "set_storage_path")
|
self.setup_mock(m, "set_storage_path")
|
||||||
user = User.objects.create_user(username="duplicate-owner")
|
user = UserFactory(username="duplicate-owner")
|
||||||
user.user_permissions.add(
|
grant_global(user, "change_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
)
|
|
||||||
first_duplicate = Document.objects.create(
|
first_duplicate = Document.objects.create(
|
||||||
checksum="owned-duplicate",
|
checksum="owned-duplicate",
|
||||||
title="First duplicate",
|
title="First duplicate",
|
||||||
@@ -1178,7 +1178,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
assign_perm("view_document", user1, self.doc2)
|
grant_object(user1, self.doc2, "view_document")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/selection_data/",
|
"/api/documents/selection_data/",
|
||||||
@@ -1188,9 +1188,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
user1.user_permissions.add(
|
grant_global(user1, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
user1 = User.objects.get(pk=user1.pk)
|
user1 = User.objects.get(pk=user1.pk)
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -1533,7 +1531,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1587,8 +1585,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
assign_perm("view_document", user1, self.doc1)
|
grant_object(user1, self.doc1, "view_document")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1609,7 +1607,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
self.assertEqual(response.content, b"Insufficient permissions")
|
self.assertEqual(response.content, b"Insufficient permissions")
|
||||||
|
|
||||||
assign_perm("change_document", user1, self.doc1)
|
grant_object(user1, self.doc1, "change_document")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/bulk_edit/",
|
"/api/documents/bulk_edit/",
|
||||||
@@ -1819,7 +1817,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1880,7 +1878,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1919,11 +1917,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
@mock.patch("documents.views.bulk_edit.merge")
|
@mock.patch("documents.views.bulk_edit.merge")
|
||||||
def test_merge_and_delete_requires_change_permission(self, m) -> None:
|
def test_merge_and_delete_requires_change_permission(self, m) -> None:
|
||||||
self.setup_mock(m, "merge")
|
self.setup_mock(m, "merge")
|
||||||
user = User.objects.create_user(username="no-change")
|
user = UserFactory(username="no-change")
|
||||||
user.user_permissions.add(
|
grant_global(user, "add_document", "delete_document")
|
||||||
Permission.objects.get(codename="add_document"),
|
|
||||||
Permission.objects.get(codename="delete_document"),
|
|
||||||
)
|
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -2310,7 +2305,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -2345,7 +2340,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
@mock.patch("documents.views.bulk_edit.edit_pdf")
|
@mock.patch("documents.views.bulk_edit.edit_pdf")
|
||||||
def test_edit_pdf_update_requires_change_permission(self, m) -> None:
|
def test_edit_pdf_update_requires_change_permission(self, m) -> None:
|
||||||
self.setup_mock(m, "edit_pdf")
|
self.setup_mock(m, "edit_pdf")
|
||||||
user = User.objects.create_user(username="no-change")
|
user = UserFactory(username="no-change")
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -2372,11 +2367,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.setup_mock(edit_pdf_mock, "edit_pdf")
|
self.setup_mock(edit_pdf_mock, "edit_pdf")
|
||||||
self.setup_mock(remove_password_mock, "remove_password")
|
self.setup_mock(remove_password_mock, "remove_password")
|
||||||
user = User.objects.create_user(username="no-delete")
|
user = UserFactory(username="no-delete")
|
||||||
user.user_permissions.add(
|
grant_global(user, "add_document", "change_document")
|
||||||
Permission.objects.get(codename="add_document"),
|
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
)
|
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
cases = [
|
cases = [
|
||||||
@@ -2463,7 +2455,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
user1 = User.objects.create(username="user1")
|
user1 = User.objects.create(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,22 @@ from typing import TYPE_CHECKING
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
|
||||||
class TestChatStreamingViewInputValidation(APITestCase):
|
class TestChatStreamingViewInputValidation(APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
def _mock_ai_enabled(self) -> mock.MagicMock:
|
def _mock_ai_enabled(self) -> mock.MagicMock:
|
||||||
@@ -113,9 +115,7 @@ class TestChatStreamingViewUnrestrictedFlag:
|
|||||||
needs to reach the view at all. Model-level only: says nothing
|
needs to reach the view at all. Model-level only: says nothing
|
||||||
about which documents (if any) this user can actually see.
|
about which documents (if any) this user can actually see.
|
||||||
"""
|
"""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_document")
|
||||||
*Permission.objects.filter(codename="view_document"),
|
|
||||||
)
|
|
||||||
return user_client
|
return user_client
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -4,23 +4,24 @@ from unittest import mock
|
|||||||
from unittest.mock import ANY
|
from unittest.mock import ANY
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||||
ENDPOINT = "/api/custom_fields/"
|
ENDPOINT = "/api/custom_fields/"
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
return super().setUp()
|
return super().setUp()
|
||||||
|
|
||||||
@@ -1174,11 +1175,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
def test_documentlink_patch_requires_change_permission_on_target_documents(
|
def test_documentlink_patch_requires_change_permission_on_target_documents(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
source_owner = User.objects.create_user(username="source-owner")
|
source_owner = UserFactory(username="source-owner")
|
||||||
source_owner.user_permissions.add(
|
grant_global(source_owner, "change_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
other_user = UserFactory(username="other-user")
|
||||||
)
|
|
||||||
other_user = User.objects.create_user(username="other-user")
|
|
||||||
|
|
||||||
source_doc = Document.objects.create(
|
source_doc = Document.objects.create(
|
||||||
title="Source",
|
title="Source",
|
||||||
@@ -1221,11 +1220,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
def test_documentlink_patch_allowed_with_change_permission_on_target_documents(
|
def test_documentlink_patch_allowed_with_change_permission_on_target_documents(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
source_owner = User.objects.create_user(username="source-owner")
|
source_owner = UserFactory(username="source-owner")
|
||||||
source_owner.user_permissions.add(
|
grant_global(source_owner, "change_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
other_user = UserFactory(username="other-user")
|
||||||
)
|
|
||||||
other_user = User.objects.create_user(username="other-user")
|
|
||||||
|
|
||||||
source_doc = Document.objects.create(
|
source_doc = Document.objects.create(
|
||||||
title="Source",
|
title="Source",
|
||||||
@@ -1244,7 +1241,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
data_type=CustomField.FieldDataType.DOCUMENTLINK,
|
data_type=CustomField.FieldDataType.DOCUMENTLINK,
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("change_document", source_owner, target_doc)
|
grant_object(source_owner, target_doc, "change_document")
|
||||||
self.client.force_authenticate(user=source_owner)
|
self.client.force_authenticate(user=source_owner)
|
||||||
|
|
||||||
resp = self.client.patch(
|
resp = self.client.patch(
|
||||||
@@ -1337,7 +1334,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(results[0]["document_count"], 1)
|
self.assertEqual(results[0]["document_count"], 1)
|
||||||
|
|
||||||
# Test as user without access to the document
|
# Test as user without access to the document
|
||||||
non_superuser = User.objects.create_user(username="non_superuser")
|
non_superuser = UserFactory(username="non_superuser")
|
||||||
non_superuser.user_permissions.add(
|
non_superuser.user_permissions.add(
|
||||||
*Permission.objects.exclude(codename="view_global_statistics"),
|
*Permission.objects.exclude(codename="view_global_statistics"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ from typing import TYPE_CHECKING
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from auditlog.models import LogEntry # type: ignore[import-untyped]
|
from auditlog.models import LogEntry # type: ignore[import-untyped]
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase as DjangoTestCase
|
from django.test import TestCase as DjangoTestCase
|
||||||
@@ -18,10 +16,12 @@ from documents.data_models import DocumentSource
|
|||||||
from documents.filters import EffectiveContentFilter
|
from documents.filters import EffectiveContentFilter
|
||||||
from documents.filters import TitleContentFilter
|
from documents.filters import TitleContentFilter
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import read_streaming_response
|
from documents.tests.utils import read_streaming_response
|
||||||
from documents.versioning import annotate_effective_content
|
from documents.versioning import annotate_effective_content
|
||||||
from documents.views import DocumentSelectionMixin
|
from documents.views import DocumentSelectionMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -31,7 +31,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
def _make_pdf_upload(self, name: str = "version.pdf") -> SimpleUploadedFile:
|
def _make_pdf_upload(self, name: str = "version.pdf") -> SimpleUploadedFile:
|
||||||
@@ -89,11 +89,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_root_endpoint_returns_403_when_user_lacks_permission(self) -> None:
|
def test_root_endpoint_returns_403_when_user_lacks_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
viewer = User.objects.create_user(username="viewer")
|
viewer = UserFactory(username="viewer")
|
||||||
viewer.user_permissions.add(
|
grant_global(viewer, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
root = Document.objects.create(
|
root = Document.objects.create(
|
||||||
title="root",
|
title="root",
|
||||||
checksum="root",
|
checksum="root",
|
||||||
@@ -283,11 +281,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(mock_backend.add_or_update.call_args[0][0].id, root.id)
|
self.assertEqual(mock_backend.add_or_update.call_args[0][0].id, root.id)
|
||||||
|
|
||||||
def test_delete_version_returns_403_without_permission(self) -> None:
|
def test_delete_version_returns_403_without_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
other = User.objects.create_user(username="other")
|
other = UserFactory(username="other")
|
||||||
other.user_permissions.add(
|
grant_global(other, "delete_document")
|
||||||
Permission.objects.get(codename="delete_document"),
|
|
||||||
)
|
|
||||||
root = Document.objects.create(
|
root = Document.objects.create(
|
||||||
title="root",
|
title="root",
|
||||||
checksum="root",
|
checksum="root",
|
||||||
@@ -371,11 +367,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertTrue(resp.data["is_root"])
|
self.assertTrue(resp.data["is_root"])
|
||||||
|
|
||||||
def test_update_version_label_returns_403_without_permission(self) -> None:
|
def test_update_version_label_returns_403_without_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
other = User.objects.create_user(username="other")
|
other = UserFactory(username="other")
|
||||||
other.user_permissions.add(
|
grant_global(other, "change_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
)
|
|
||||||
root = Document.objects.create(
|
root = Document.objects.create(
|
||||||
title="root",
|
title="root",
|
||||||
checksum="root",
|
checksum="root",
|
||||||
@@ -553,11 +547,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_metadata_returns_403_when_user_lacks_permission(self) -> None:
|
def test_metadata_returns_403_when_user_lacks_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
other = User.objects.create_user(username="other")
|
other = UserFactory(username="other")
|
||||||
other.user_permissions.add(
|
grant_global(other, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="root",
|
title="root",
|
||||||
checksum="root",
|
checksum="root",
|
||||||
@@ -653,8 +645,8 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(resp.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
self.assertEqual(resp.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def test_update_version_returns_403_without_permission(self) -> None:
|
def test_update_version_returns_403_without_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
other = User.objects.create_user(username="other")
|
other = UserFactory(username="other")
|
||||||
root = Document.objects.create(
|
root = Document.objects.create(
|
||||||
title="root",
|
title="root",
|
||||||
checksum="root",
|
checksum="root",
|
||||||
@@ -672,8 +664,8 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_update_version_requires_global_change_permission(self) -> None:
|
def test_update_version_requires_global_change_permission(self) -> None:
|
||||||
user = User.objects.create_user(username="add-only")
|
user = UserFactory(username="add-only")
|
||||||
user.user_permissions.add(Permission.objects.get(codename="add_document"))
|
grant_global(user, "add_document")
|
||||||
root = Document.objects.create(
|
root = Document.objects.create(
|
||||||
title="root",
|
title="root",
|
||||||
checksum="root",
|
checksum="root",
|
||||||
@@ -978,7 +970,7 @@ class TestVersionAwareFilters(DjangoTestCase):
|
|||||||
superseded content -- selecting documents the list view, filtered by
|
superseded content -- selecting documents the list view, filtered by
|
||||||
the same term, does not show.
|
the same term, does not show.
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_superuser(username="bulk_selection")
|
user = UserFactory(username="bulk_selection", superuser=True)
|
||||||
|
|
||||||
selected = DocumentSelectionMixin()._resolve_document_ids(
|
selected = DocumentSelectionMixin()._resolve_document_ids(
|
||||||
user=user,
|
user=user,
|
||||||
@@ -1005,7 +997,7 @@ class TestBulkSelectionExcludesVersions(DjangoTestCase):
|
|||||||
"Select all matching" reconstructs the document list, which never
|
"Select all matching" reconstructs the document list, which never
|
||||||
contains version documents as rows of their own.
|
contains version documents as rows of their own.
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_superuser(username="bulk_versions")
|
user = UserFactory(username="bulk_versions", superuser=True)
|
||||||
root = Document.objects.create(
|
root = Document.objects.create(
|
||||||
title="shared-title root",
|
title="shared-title root",
|
||||||
checksum="bulk-root",
|
checksum="bulk-root",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
|||||||
from django.db import DataError
|
from django.db import DataError
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@@ -48,18 +47,22 @@ from documents.models import Workflow
|
|||||||
from documents.models import WorkflowAction
|
from documents.models import WorkflowAction
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.signals.handlers import run_workflows
|
from documents.signals.handlers import run_workflows
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.factories import TagFactory
|
|
||||||
from documents.tests.utils import ConsumeTaskMixin
|
from documents.tests.utils import ConsumeTaskMixin
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import read_streaming_response
|
from documents.tests.utils import read_streaming_response
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_all_global
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
@@ -357,10 +360,10 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
with Path(filename).open("wb") as f:
|
with Path(filename).open("wb") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user1, "view_document")
|
||||||
user2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user2, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user2)
|
self.client.force_authenticate(user2)
|
||||||
|
|
||||||
@@ -383,7 +386,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
|
response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
assign_perm("view_document", user2, doc)
|
grant_object(user2, doc, "view_document")
|
||||||
|
|
||||||
response = self.client.get(f"/api/documents/{doc.pk}/download/")
|
response = self.client.get(f"/api/documents/{doc.pk}/download/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@@ -760,8 +763,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
- History is returned
|
- History is returned
|
||||||
"""
|
"""
|
||||||
# No auditlog permissions
|
# No auditlog permissions
|
||||||
user = User.objects.create_user(username="test")
|
user = UserFactory(username="test")
|
||||||
user.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user, "view_document")
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="First title",
|
title="First title",
|
||||||
@@ -776,7 +779,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
# superuser
|
# superuser
|
||||||
user.is_superuser = True
|
user.is_superuser = True
|
||||||
user.save()
|
user.save()
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
doc2 = Document.objects.create(
|
doc2 = Document.objects.create(
|
||||||
title="Second title",
|
title="Second title",
|
||||||
checksum="456",
|
checksum="456",
|
||||||
@@ -1073,11 +1076,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertEqual(response.data["duplicate_documents"], [])
|
self.assertEqual(response.data["duplicate_documents"], [])
|
||||||
|
|
||||||
def test_has_duplicates_filter_respects_document_permissions(self) -> None:
|
def test_has_duplicates_filter_respects_document_permissions(self) -> None:
|
||||||
owner = User.objects.create_user(username="duplicate-owner")
|
owner = UserFactory(username="duplicate-owner")
|
||||||
requester = User.objects.create_user(username="duplicate-requester")
|
requester = UserFactory(username="duplicate-requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
visible_document = Document.objects.create(
|
visible_document = Document.objects.create(
|
||||||
title="visible document",
|
title="visible document",
|
||||||
checksum="permission-match",
|
checksum="permission-match",
|
||||||
@@ -1096,7 +1097,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
[document["id"] for document in response.data["results"]],
|
[document["id"] for document in response.data["results"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("view_document", requester, hidden_duplicate)
|
grant_object(requester, hidden_duplicate, "view_document")
|
||||||
response = self.client.get("/api/documents/?has_duplicates=true")
|
response = self.client.get("/api/documents/?has_duplicates=true")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
visible_document.id,
|
visible_document.id,
|
||||||
@@ -1317,10 +1318,10 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Owner filters work correctly but still respect permissions
|
- Owner filters work correctly but still respect permissions
|
||||||
"""
|
"""
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u1, "view_document")
|
||||||
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u2, "view_document")
|
||||||
|
|
||||||
u1_doc1 = Document.objects.create(
|
u1_doc1 = Document.objects.create(
|
||||||
title="none1",
|
title="none1",
|
||||||
@@ -1353,7 +1354,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.client.force_authenticate(user=u1)
|
self.client.force_authenticate(user=u1)
|
||||||
assign_perm("view_document", u1, u2_doc2)
|
grant_object(u1, u2_doc2, "view_document")
|
||||||
|
|
||||||
# Will not show any u1 docs or u2_doc1 which isn't shared
|
# Will not show any u1 docs or u2_doc1 which isn't shared
|
||||||
response = self.client.get(f"/api/documents/?owner__id__none={u1.id}")
|
response = self.client.get(f"/api/documents/?owner__id__none={u1.id}")
|
||||||
@@ -1400,7 +1401,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
[u1_doc1.id, u1_doc2.id, u2_doc2.id],
|
[u1_doc1.id, u1_doc2.id, u2_doc2.id],
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("view_document", u2, u1_doc1)
|
grant_object(u2, u1_doc1, "view_document")
|
||||||
|
|
||||||
# Will show only documents shared by user
|
# Will show only documents shared by user
|
||||||
response = self.client.get(f"/api/documents/?shared_by__id={u1.id}")
|
response = self.client.get(f"/api/documents/?shared_by__id={u1.id}")
|
||||||
@@ -1424,8 +1425,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
- The document is returned exactly once, not once per permission path
|
- The document is returned exactly once, not once per permission path
|
||||||
(regression test for https://github.com/paperless-ngx/paperless-ngx/issues/13331)
|
(regression test for https://github.com/paperless-ngx/paperless-ngx/issues/13331)
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user("user1")
|
user = UserFactory(username="user1")
|
||||||
user.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user, "view_document")
|
||||||
group = Group.objects.create(name="group1")
|
group = Group.objects.create(name="group1")
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
|
|
||||||
@@ -1433,7 +1434,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
tag2 = TagFactory()
|
tag2 = TagFactory()
|
||||||
doc = DocumentFactory(title="shared", owner=user)
|
doc = DocumentFactory(title="shared", owner=user)
|
||||||
doc.tags.add(tag1, tag2)
|
doc.tags.add(tag1, tag2)
|
||||||
assign_perm("view_document", group, doc)
|
grant_object(group, doc, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -1452,11 +1453,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- The document does not appear in their results
|
- The document does not appear in their results
|
||||||
"""
|
"""
|
||||||
owner = User.objects.create_user("owner1")
|
owner = UserFactory(username="owner1")
|
||||||
stranger = User.objects.create_user("stranger1")
|
stranger = UserFactory(username="stranger1")
|
||||||
stranger.user_permissions.add(
|
grant_global(stranger, "view_document")
|
||||||
*Permission.objects.filter(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
DocumentFactory(title="private", owner=owner)
|
DocumentFactory(title="private", owner=owner)
|
||||||
|
|
||||||
@@ -1474,17 +1473,17 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Only the group member sees the document
|
- Only the group member sees the document
|
||||||
"""
|
"""
|
||||||
owner = User.objects.create_user("owner2")
|
owner = UserFactory(username="owner2")
|
||||||
member = User.objects.create_user("member1")
|
member = UserFactory(username="member1")
|
||||||
non_member = User.objects.create_user("nonmember1")
|
non_member = UserFactory(username="nonmember1")
|
||||||
for u in (member, non_member):
|
for u in (member, non_member):
|
||||||
u.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u, "view_document")
|
||||||
|
|
||||||
group = Group.objects.create(name="group2")
|
group = Group.objects.create(name="group2")
|
||||||
member.groups.add(group)
|
member.groups.add(group)
|
||||||
|
|
||||||
doc = DocumentFactory(title="shared2", owner=owner)
|
doc = DocumentFactory(title="shared2", owner=owner)
|
||||||
assign_perm("view_document", group, doc)
|
grant_object(group, doc, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user=member)
|
self.client.force_authenticate(user=member)
|
||||||
response = self.client.get("/api/documents/")
|
response = self.client.get("/api/documents/")
|
||||||
@@ -1785,8 +1784,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Statistics only include inbox counts for tags accessible by the user
|
- Statistics only include inbox counts for tags accessible by the user
|
||||||
"""
|
"""
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
inbox_tag_u1 = Tag.objects.create(name="inbox_u1", is_inbox_tag=True, owner=u1)
|
inbox_tag_u1 = Tag.objects.create(name="inbox_u1", is_inbox_tag=True, owner=u1)
|
||||||
Tag.objects.create(name="inbox_u2", is_inbox_tag=True, owner=u2)
|
Tag.objects.create(name="inbox_u2", is_inbox_tag=True, owner=u2)
|
||||||
doc_u1 = Document.objects.create(
|
doc_u1 = Document.objects.create(
|
||||||
@@ -1816,11 +1815,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertEqual(response.data["documents_inbox"], 0)
|
self.assertEqual(response.data["documents_inbox"], 0)
|
||||||
|
|
||||||
def test_statistics_with_statistics_permission(self) -> None:
|
def test_statistics_with_statistics_permission(self) -> None:
|
||||||
owner = User.objects.create_user("owner")
|
owner = UserFactory(username="owner")
|
||||||
stats_user = User.objects.create_user("stats-user")
|
stats_user = UserFactory(username="stats-user")
|
||||||
stats_user.user_permissions.add(
|
grant_global(stats_user, "view_global_statistics")
|
||||||
Permission.objects.get(codename="view_global_statistics"),
|
|
||||||
)
|
|
||||||
|
|
||||||
inbox_tag = Tag.objects.create(
|
inbox_tag = Tag.objects.create(
|
||||||
name="stats_inbox",
|
name="stats_inbox",
|
||||||
@@ -1986,7 +1983,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
def test_upload_insufficient_permissions(self) -> None:
|
def test_upload_insufficient_permissions(self) -> None:
|
||||||
self.client.force_authenticate(user=User.objects.create_user("testuser2"))
|
self.client.force_authenticate(user=UserFactory(username="testuser2"))
|
||||||
|
|
||||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -2782,9 +2779,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
mock_get_date_parser.assert_not_called()
|
mock_get_date_parser.assert_not_called()
|
||||||
|
|
||||||
def test_saved_views(self) -> None:
|
def test_saved_views(self) -> None:
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
u3 = User.objects.create_user("user3")
|
u3 = UserFactory(username="user3")
|
||||||
|
|
||||||
view_perm = Permission.objects.get(codename="view_savedview")
|
view_perm = Permission.objects.get(codename="view_savedview")
|
||||||
change_perm = Permission.objects.get(codename="change_savedview")
|
change_perm = Permission.objects.get(codename="change_savedview")
|
||||||
@@ -2807,9 +2804,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
sort_field="",
|
sort_field="",
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("view_savedview", u1, v2)
|
grant_object(u1, v2, "view_savedview")
|
||||||
assign_perm("change_savedview", u1, v2)
|
grant_object(u1, v2, "change_savedview")
|
||||||
assign_perm("view_savedview", u1, v3)
|
grant_object(u1, v3, "view_savedview")
|
||||||
|
|
||||||
self.client.force_authenticate(user=u1)
|
self.client.force_authenticate(user=u1)
|
||||||
|
|
||||||
@@ -3064,7 +3061,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertListEqual(saved_view_settings["sidebar_views_visible_ids"], [v2.id])
|
self.assertListEqual(saved_view_settings["sidebar_views_visible_ids"], [v2.id])
|
||||||
|
|
||||||
def test_saved_view_create_update_patch(self) -> None:
|
def test_saved_view_create_update_patch(self) -> None:
|
||||||
User.objects.create_user("user1")
|
UserFactory(username="user1")
|
||||||
|
|
||||||
view = {
|
view = {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
@@ -3127,7 +3124,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
- Display options are updated
|
- Display options are updated
|
||||||
- Display fields are validated
|
- Display fields are validated
|
||||||
"""
|
"""
|
||||||
User.objects.create_user("user1")
|
UserFactory(username="user1")
|
||||||
|
|
||||||
view = {
|
view = {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
@@ -3568,11 +3565,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Notes are neither created nor deleted
|
- Notes are neither created nor deleted
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user2.save()
|
user2.save()
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@@ -3592,7 +3589,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertEqual(resp.content, b"Insufficient permissions to view notes")
|
self.assertEqual(resp.content, b"Insufficient permissions to view notes")
|
||||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
assign_perm("view_document", user1, doc)
|
grant_object(user1, doc, "view_document")
|
||||||
|
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
f"/api/documents/{doc.pk}/notes/",
|
f"/api/documents/{doc.pk}/notes/",
|
||||||
@@ -3616,12 +3613,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_notes_require_global_document_permissions(self) -> None:
|
def test_notes_require_global_document_permissions(self) -> None:
|
||||||
user = User.objects.create_user(username="note_editor")
|
user = UserFactory(username="note_editor")
|
||||||
user.user_permissions.add(
|
grant_global(user, "view_note", "add_note", "delete_note")
|
||||||
*Permission.objects.filter(
|
|
||||||
codename__in=["view_note", "add_note", "delete_note"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="test",
|
title="test",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
@@ -3634,9 +3627,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
response = self.client.get(f"/api/documents/{doc.pk}/notes/")
|
response = self.client.get(f"/api/documents/{doc.pk}/notes/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
user.user_permissions.add(
|
grant_global(user, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
user = User.objects.get(pk=user.pk)
|
user = User.objects.get(pk=user.pk)
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
response = self.client.get(f"/api/documents/{doc.pk}/notes/")
|
response = self.client.get(f"/api/documents/{doc.pk}/notes/")
|
||||||
@@ -3648,9 +3639,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
user.user_permissions.add(
|
grant_global(user, "change_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
)
|
|
||||||
user = User.objects.get(pk=user.pk)
|
user = User.objects.get(pk=user.pk)
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -3797,12 +3786,12 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
- Unique items are created
|
- Unique items are created
|
||||||
- Non-unique items are not allowed
|
- Non-unique items are not allowed
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
|
grant_global(user1, "add_tag")
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user2.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
|
grant_global(user2, "add_tag")
|
||||||
user2.save()
|
user2.save()
|
||||||
|
|
||||||
# User 1 creates tag 1 owned by user 1 by default
|
# User 1 creates tag 1 owned by user 1 by default
|
||||||
@@ -3857,12 +3846,12 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
- Unique items are created
|
- Unique items are created
|
||||||
- Non-unique items are not allowed on update
|
- Non-unique items are not allowed on update
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
|
grant_global(user1, "change_tag")
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user2.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
|
grant_global(user2, "change_tag")
|
||||||
user2.save()
|
user2.save()
|
||||||
|
|
||||||
# Create name tag 1 owned by user 1
|
# Create name tag 1 owned by user 1
|
||||||
@@ -3993,11 +3982,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Links only shown if user has permissions
|
- Links only shown if user has permissions
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user2.save()
|
user2.save()
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@@ -4017,7 +4006,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
self.assertEqual(resp.content, b"Insufficient permissions to add share link")
|
self.assertEqual(resp.content, b"Insufficient permissions to add share link")
|
||||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
assign_perm("change_document", user1, doc)
|
grant_object(user1, doc, "change_document")
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/api/documents/{doc.pk}/share_links/",
|
f"/api/documents/{doc.pk}/share_links/",
|
||||||
@@ -4034,11 +4023,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Share link creation is denied until view permission is granted
|
- Share link creation is denied until view permission is granted
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="add_sharelink"))
|
grant_global(user1, "add_sharelink")
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user2.save()
|
user2.save()
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@@ -4060,7 +4049,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
assign_perm("view_document", user1, doc)
|
grant_object(user1, doc, "view_document")
|
||||||
|
|
||||||
create_resp = self.client.post(
|
create_resp = self.client.post(
|
||||||
"/api/share_links/",
|
"/api/share_links/",
|
||||||
@@ -4072,9 +4061,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
user1.user_permissions.add(
|
grant_global(user1, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
user1 = User.objects.get(pk=user1.pk)
|
user1 = User.objects.get(pk=user1.pk)
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
create_resp = self.client.post(
|
create_resp = self.client.post(
|
||||||
@@ -4097,11 +4084,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- ASN +1 from user2's doc is returned for user1
|
- ASN +1 from user2's doc is returned for user1
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user2.save()
|
user2.save()
|
||||||
|
|
||||||
doc1 = Document.objects.create(
|
doc1 = Document.objects.create(
|
||||||
@@ -4141,8 +4128,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- ASN 1 is returned
|
- ASN 1 is returned
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
doc1 = Document.objects.create(
|
doc1 = Document.objects.create(
|
||||||
@@ -4170,7 +4157,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Explicit error is returned
|
- Explicit error is returned
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_superuser(username="test1")
|
user1 = UserFactory(username="test1", superuser=True)
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -4348,8 +4335,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Error response is returned
|
- Error response is returned
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@@ -4454,7 +4441,7 @@ class TestDocumentApiTagColors(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
@@ -4534,7 +4521,7 @@ class TestDocumentApiCustomFieldsSorting(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
self.doc1 = Document.objects.create(
|
self.doc1 = Document.objects.create(
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import json
|
|||||||
import shutil
|
import shutil
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
|
|
||||||
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
||||||
@@ -20,7 +20,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
self.doc1 = Document.objects.create(
|
self.doc1 = Document.objects.create(
|
||||||
@@ -303,8 +303,8 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Forbidden response is returned
|
- Forbidden response is returned
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user1, "view_document")
|
||||||
|
|
||||||
doc_owned = Document.objects.create(
|
doc_owned = Document.objects.create(
|
||||||
title="owned_doc",
|
title="owned_doc",
|
||||||
@@ -338,8 +338,8 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Request succeeds
|
- Request succeeds
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user1, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from collections.abc import Callable
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
@@ -13,7 +12,8 @@ from documents.models import Document
|
|||||||
from documents.models import SavedView
|
from documents.models import SavedView
|
||||||
from documents.models import SavedViewFilterRule
|
from documents.models import SavedViewFilterRule
|
||||||
from documents.serialisers import DocumentSerializer
|
from documents.serialisers import DocumentSerializer
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class DocumentWrapper:
|
class DocumentWrapper:
|
||||||
@@ -35,7 +35,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
# Create one custom field per type. The fields are called f"{type}_field".
|
# Create one custom field per type. The fields are called f"{type}_field".
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import json
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from guardian.shortcuts import get_groups_with_perms
|
from guardian.shortcuts import get_groups_with_perms
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -21,14 +19,17 @@ from documents.models import Document
|
|||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestApiObjects(DirectoriesMixin, APITestCase):
|
class TestApiObjects(DirectoriesMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
user = User.objects.create_superuser(username="temp_admin")
|
user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True)
|
self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True)
|
||||||
@@ -153,7 +154,7 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
# A newer document owned by another user must not leak through the
|
# A newer document owned by another user must not leak through the
|
||||||
# aggregate for a non-superuser who cannot view it
|
# aggregate for a non-superuser who cannot view it
|
||||||
other = User.objects.create_user(username="other")
|
other = UserFactory(username="other")
|
||||||
Document.objects.create(
|
Document.objects.create(
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
correspondent=self.c1,
|
correspondent=self.c1,
|
||||||
@@ -162,10 +163,8 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
owner=other,
|
owner=other,
|
||||||
)
|
)
|
||||||
|
|
||||||
user = User.objects.create_user(username="regular")
|
user = UserFactory(username="regular")
|
||||||
user.user_permissions.add(
|
grant_global(user, "view_correspondent")
|
||||||
Permission.objects.get(codename="view_correspondent"),
|
|
||||||
)
|
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
response = self.client.get("/api/correspondents/?last_correspondence=true")
|
response = self.client.get("/api/correspondents/?last_correspondence=true")
|
||||||
@@ -200,7 +199,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
user = User.objects.create_superuser(username="temp_admin")
|
user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
|
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
|
||||||
@@ -455,11 +454,9 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.data, "folder/Something.pdf")
|
self.assertEqual(response.data, "folder/Something.pdf")
|
||||||
|
|
||||||
def test_test_storage_path_requires_document_view_permission(self) -> None:
|
def test_test_storage_path_requires_document_view_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
unprivileged = User.objects.create_user(username="unprivileged")
|
unprivileged = UserFactory(username="unprivileged")
|
||||||
unprivileged.user_permissions.add(
|
grant_global(unprivileged, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
@@ -481,15 +478,15 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
self.assertIn("document", response.data)
|
self.assertIn("document", response.data)
|
||||||
|
|
||||||
def test_test_storage_path_allows_shared_document_view_permission(self) -> None:
|
def test_test_storage_path_allows_shared_document_view_permission(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
viewer = User.objects.create_user(username="viewer")
|
viewer = UserFactory(username="viewer")
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
title="Shared",
|
title="Shared",
|
||||||
checksum="123",
|
checksum="123",
|
||||||
)
|
)
|
||||||
assign_perm("view_document", viewer, document)
|
grant_object(viewer, document, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user=viewer)
|
self.client.force_authenticate(user=viewer)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -504,9 +501,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
viewer.user_permissions.add(
|
grant_global(viewer, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
viewer = User.objects.get(pk=viewer.pk)
|
viewer = User.objects.get(pk=viewer.pk)
|
||||||
self.client.force_authenticate(user=viewer)
|
self.client.force_authenticate(user=viewer)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -545,14 +540,12 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
def test_test_storage_path_exposes_basic_document_context_but_not_sensitive_owner_data(
|
def test_test_storage_path_exposes_basic_document_context_but_not_sensitive_owner_data(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
owner = User.objects.create_user(
|
owner = UserFactory(
|
||||||
username="owner",
|
username="owner",
|
||||||
password="password",
|
password="password",
|
||||||
email="owner@example.com",
|
email="owner@example.com",
|
||||||
)
|
)
|
||||||
owner.user_permissions.add(
|
grant_global(owner, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
@@ -614,8 +607,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
def test_test_storage_path_includes_related_objects_for_visible_document(
|
def test_test_storage_path_includes_related_objects_for_visible_document(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
viewer = User.objects.create_user(username="viewer")
|
viewer = UserFactory(username="viewer")
|
||||||
private_correspondent = Correspondent.objects.create(
|
private_correspondent = Correspondent.objects.create(
|
||||||
name="Private Correspondent",
|
name="Private Correspondent",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
@@ -627,10 +620,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
title="Document",
|
title="Document",
|
||||||
checksum="123",
|
checksum="123",
|
||||||
)
|
)
|
||||||
assign_perm("view_document", viewer, document)
|
grant_object(viewer, document, "view_document")
|
||||||
viewer.user_permissions.add(
|
grant_global(viewer, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=viewer)
|
self.client.force_authenticate(user=viewer)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -662,7 +653,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.data, "Private Correspondent.pdf")
|
self.assertEqual(response.data, "Private Correspondent.pdf")
|
||||||
|
|
||||||
def test_test_storage_path_superuser_can_view_private_related_objects(self) -> None:
|
def test_test_storage_path_superuser_can_view_private_related_objects(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
private_correspondent = Correspondent.objects.create(
|
private_correspondent = Correspondent.objects.create(
|
||||||
name="Private Correspondent",
|
name="Private Correspondent",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
@@ -693,8 +684,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
def test_test_storage_path_includes_doc_type_storage_path_and_tags(
|
def test_test_storage_path_includes_doc_type_storage_path_and_tags(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
viewer = User.objects.create_user(username="viewer")
|
viewer = UserFactory(username="viewer")
|
||||||
private_document_type = DocumentType.objects.create(
|
private_document_type = DocumentType.objects.create(
|
||||||
name="Private Type",
|
name="Private Type",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
@@ -717,10 +708,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
checksum="123",
|
checksum="123",
|
||||||
)
|
)
|
||||||
document.tags.add(private_tag)
|
document.tags.add(private_tag)
|
||||||
assign_perm("view_document", viewer, document)
|
grant_object(viewer, document, "view_document")
|
||||||
viewer.user_permissions.add(
|
grant_global(viewer, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=viewer)
|
self.client.force_authenticate(user=viewer)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -756,8 +745,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
def test_test_storage_path_includes_custom_fields_for_visible_document(
|
def test_test_storage_path_includes_custom_fields_for_visible_document(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
viewer = User.objects.create_user(username="viewer")
|
viewer = UserFactory(username="viewer")
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
@@ -773,10 +762,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
field=custom_field,
|
field=custom_field,
|
||||||
value_int=42,
|
value_int=42,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", viewer, document)
|
grant_object(viewer, document, "view_document")
|
||||||
viewer.user_permissions.add(
|
grant_global(viewer, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=viewer)
|
self.client.force_authenticate(user=viewer)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -798,7 +785,7 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.temp_admin = User.objects.create_superuser(username="temp_admin")
|
self.temp_admin = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.temp_admin)
|
self.client.force_authenticate(user=self.temp_admin)
|
||||||
|
|
||||||
self.t1 = Tag.objects.create(name="t1")
|
self.t1 = Tag.objects.create(name="t1")
|
||||||
@@ -1030,9 +1017,7 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User is able to delete objects
|
- User is able to delete objects
|
||||||
"""
|
"""
|
||||||
self.user1.user_permissions.add(
|
grant_global(self.user1, "delete_tag")
|
||||||
*Permission.objects.filter(codename="delete_tag"),
|
|
||||||
)
|
|
||||||
self.user1.save()
|
self.user1.save()
|
||||||
self.client.force_authenticate(user=self.user1)
|
self.client.force_authenticate(user=self.user1)
|
||||||
|
|
||||||
@@ -1062,9 +1047,7 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
self.t2.owner = User.objects.get(username="temp_admin")
|
self.t2.owner = User.objects.get(username="temp_admin")
|
||||||
self.t2.save()
|
self.t2.save()
|
||||||
|
|
||||||
self.user1.user_permissions.add(
|
grant_global(self.user1, "delete_tag")
|
||||||
*Permission.objects.filter(codename="delete_tag"),
|
|
||||||
)
|
|
||||||
self.user1.save()
|
self.user1.save()
|
||||||
self.client.force_authenticate(user=self.user1)
|
self.client.force_authenticate(user=self.user1)
|
||||||
|
|
||||||
@@ -1097,9 +1080,7 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
self.t2.owner = User.objects.get(username="temp_admin")
|
self.t2.owner = User.objects.get(username="temp_admin")
|
||||||
self.t2.save()
|
self.t2.save()
|
||||||
|
|
||||||
self.user1.user_permissions.add(
|
grant_global(self.user1, "delete_tag")
|
||||||
*Permission.objects.filter(codename="delete_tag"),
|
|
||||||
)
|
|
||||||
self.user1.save()
|
self.user1.save()
|
||||||
self.client.force_authenticate(user=self.user1)
|
self.client.force_authenticate(user=self.user1)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ from unittest import mock
|
|||||||
from allauth.mfa.models import Authenticator
|
from allauth.mfa.models import Authenticator
|
||||||
from allauth.mfa.totp.internal import auth as totp_auth
|
from allauth.mfa.totp.internal import auth as totp_auth
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from guardian.shortcuts import get_perms
|
from guardian.shortcuts import get_perms
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -19,7 +17,11 @@ from documents.models import DocumentType
|
|||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_all_global
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestApiAuth(DirectoriesMixin, APITestCase):
|
class TestApiAuth(DirectoriesMixin, APITestCase):
|
||||||
@@ -93,14 +95,14 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
self.assertNotIn("X-Version", response)
|
self.assertNotIn("X-Version", response)
|
||||||
|
|
||||||
def test_api_version_with_auth(self) -> None:
|
def test_api_version_with_auth(self) -> None:
|
||||||
user = User.objects.create_superuser(username="test")
|
user = UserFactory(username="test", superuser=True)
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
response = self.client.get("/api/documents/")
|
response = self.client.get("/api/documents/")
|
||||||
self.assertIn("X-Api-Version", response)
|
self.assertIn("X-Api-Version", response)
|
||||||
self.assertIn("X-Version", response)
|
self.assertIn("X-Version", response)
|
||||||
|
|
||||||
def test_api_insufficient_permissions(self) -> None:
|
def test_api_insufficient_permissions(self) -> None:
|
||||||
user = User.objects.create_user(username="test")
|
user = UserFactory(username="test")
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
Document.objects.create(title="Test")
|
Document.objects.create(title="Test")
|
||||||
@@ -137,8 +139,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_api_sufficient_permissions(self) -> None:
|
def test_api_sufficient_permissions(self) -> None:
|
||||||
user = User.objects.create_user(username="test")
|
user = UserFactory(username="test")
|
||||||
user.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user)
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
@@ -166,9 +168,9 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_api_get_object_permissions(self) -> None:
|
def test_api_get_object_permissions(self) -> None:
|
||||||
user1 = User.objects.create_user(username="test1")
|
user1 = UserFactory(username="test1")
|
||||||
user2 = User.objects.create_user(username="test2")
|
user2 = UserFactory(username="test2")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user1, "view_document")
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -205,7 +207,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Object created with current user as owner
|
- Object created with current user as owner
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_superuser(username="user1")
|
user1 = UserFactory(username="user1", superuser=True)
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -234,7 +236,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Object created with no owner
|
- Object created with no owner
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_superuser(username="user1")
|
user1 = UserFactory(username="user1", superuser=True)
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -265,7 +267,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Object permissions are set appropriately
|
- Object permissions are set appropriately
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_superuser(username="user1")
|
user1 = UserFactory(username="user1", superuser=True)
|
||||||
user2 = User.objects.create(username="user2")
|
user2 = User.objects.create(username="user2")
|
||||||
group1 = Group.objects.create(name="group1")
|
group1 = Group.objects.create(name="group1")
|
||||||
|
|
||||||
@@ -313,7 +315,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Object permissions are set appropriately
|
- Object permissions are set appropriately
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_superuser(username="user1")
|
user1 = UserFactory(username="user1", superuser=True)
|
||||||
user2 = User.objects.create(username="user2")
|
user2 = User.objects.create(username="user2")
|
||||||
group1 = Group.objects.create(name="group1")
|
group1 = Group.objects.create(name="group1")
|
||||||
|
|
||||||
@@ -363,7 +365,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
content="this is a document",
|
content="this is a document",
|
||||||
)
|
)
|
||||||
user1 = User.objects.create_superuser(username="user1")
|
user1 = UserFactory(username="user1", superuser=True)
|
||||||
user2 = User.objects.create(username="user2")
|
user2 = User.objects.create(username="user2")
|
||||||
group1 = Group.objects.create(name="group1")
|
group1 = Group.objects.create(name="group1")
|
||||||
|
|
||||||
@@ -413,16 +415,16 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
content="this is a document",
|
content="this is a document",
|
||||||
)
|
)
|
||||||
user1 = User.objects.create_superuser(username="user1")
|
user1 = UserFactory(username="user1", superuser=True)
|
||||||
user2 = User.objects.create(username="user2")
|
user2 = User.objects.create(username="user2")
|
||||||
group1 = Group.objects.create(name="group1")
|
group1 = Group.objects.create(name="group1")
|
||||||
doc.owner = user1
|
doc.owner = user1
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|
||||||
assign_perm("view_document", user2, doc)
|
grant_object(user2, doc, "view_document")
|
||||||
assign_perm("change_document", user2, doc)
|
grant_object(user2, doc, "change_document")
|
||||||
assign_perm("view_document", group1, doc)
|
grant_object(group1, doc, "view_document")
|
||||||
assign_perm("change_document", group1, doc)
|
grant_object(group1, doc, "change_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -446,11 +448,9 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
self.assertIn("change_document", get_perms(group1, doc))
|
self.assertIn("change_document", get_perms(group1, doc))
|
||||||
|
|
||||||
def test_document_permissions_change_requires_owner(self) -> None:
|
def test_document_permissions_change_requires_owner(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
editor = User.objects.create_user(username="editor")
|
editor = UserFactory(username="editor")
|
||||||
editor.user_permissions.add(
|
grant_all_global(editor)
|
||||||
*Permission.objects.all(),
|
|
||||||
)
|
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="Ownered doc",
|
title="Ownered doc",
|
||||||
@@ -460,8 +460,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("view_document", editor, doc)
|
grant_object(editor, doc, "view_document")
|
||||||
assign_perm("change_document", editor, doc)
|
grant_object(editor, doc, "change_document")
|
||||||
|
|
||||||
self.client.force_authenticate(editor)
|
self.client.force_authenticate(editor)
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
@@ -499,9 +499,9 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_dynamic_permissions_fields(self) -> None:
|
def test_dynamic_permissions_fields(self) -> None:
|
||||||
user1 = User.objects.create_user(username="user1")
|
user1 = UserFactory(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(user1, "view_document")
|
||||||
user2 = User.objects.create_user(username="user2")
|
user2 = UserFactory(username="user2")
|
||||||
|
|
||||||
Document.objects.create(title="Test", content="content 1", checksum="1")
|
Document.objects.create(title="Test", content="content 1", checksum="1")
|
||||||
doc2 = Document.objects.create(
|
doc2 = Document.objects.create(
|
||||||
@@ -523,10 +523,10 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
owner=user1,
|
owner=user1,
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("view_document", user1, doc2)
|
grant_object(user1, doc2, "view_document")
|
||||||
assign_perm("view_document", user1, doc3)
|
grant_object(user1, doc3, "view_document")
|
||||||
assign_perm("change_document", user1, doc3)
|
grant_object(user1, doc3, "change_document")
|
||||||
assign_perm("view_document", user2, doc4)
|
grant_object(user2, doc4, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -574,8 +574,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
owned by someone else with no explicit guardian grant -- mirrors
|
owned by someone else with no explicit guardian grant -- mirrors
|
||||||
guardian's own ObjectPermissionChecker.has_perm() superuser shortcut.
|
guardian's own ObjectPermissionChecker.has_perm() superuser shortcut.
|
||||||
"""
|
"""
|
||||||
superuser = User.objects.create_superuser(username="admin")
|
superuser = UserFactory(username="admin", superuser=True)
|
||||||
other_user = User.objects.create_user(username="user2")
|
other_user = UserFactory(username="user2")
|
||||||
Document.objects.create(
|
Document.objects.create(
|
||||||
title="Test",
|
title="Test",
|
||||||
content="content",
|
content="content",
|
||||||
@@ -602,7 +602,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- MFA required error is returned
|
- MFA required error is returned
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="user1")
|
user1 = UserFactory(username="user1")
|
||||||
user1.set_password("password")
|
user1.set_password("password")
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- MFA code is required
|
- MFA code is required
|
||||||
"""
|
"""
|
||||||
user1 = User.objects.create_user(username="user1")
|
user1 = UserFactory(username="user1")
|
||||||
user1.set_password("password")
|
user1.set_password("password")
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ class TestApiUser(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
def test_get_users(self) -> None:
|
def test_get_users(self) -> None:
|
||||||
@@ -858,10 +858,8 @@ class TestApiUser(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
regular_user = User.objects.create_user(username="regular_user")
|
regular_user = UserFactory(username="regular_user")
|
||||||
regular_user.user_permissions.add(
|
grant_all_global(regular_user)
|
||||||
*Permission.objects.all(),
|
|
||||||
)
|
|
||||||
self.client.force_authenticate(regular_user)
|
self.client.force_authenticate(regular_user)
|
||||||
Authenticator.objects.create(
|
Authenticator.objects.create(
|
||||||
user=user1,
|
user=user1,
|
||||||
@@ -885,9 +883,9 @@ class TestApiUser(DirectoriesMixin, APITestCase):
|
|||||||
- Only superusers can change superuser status
|
- Only superusers can change superuser status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user1 = User.objects.create_user(username="user1")
|
user1 = UserFactory(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user2 = User.objects.create_superuser(username="user2")
|
user2 = UserFactory(username="user2", superuser=True)
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -972,9 +970,9 @@ class TestApiUser(DirectoriesMixin, APITestCase):
|
|||||||
- Only superusers can change staff status
|
- Only superusers can change staff status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user1 = User.objects.create_user(username="user1")
|
user1 = UserFactory(username="user1")
|
||||||
user1.user_permissions.add(*Permission.objects.all())
|
grant_all_global(user1)
|
||||||
user2 = User.objects.create_superuser(username="user2")
|
user2 = UserFactory(username="user2", superuser=True)
|
||||||
|
|
||||||
self.client.force_authenticate(user1)
|
self.client.force_authenticate(user1)
|
||||||
|
|
||||||
@@ -1027,7 +1025,7 @@ class TestApiGroup(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
def test_get_groups(self) -> None:
|
def test_get_groups(self) -> None:
|
||||||
@@ -1128,7 +1126,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.temp_admin = User.objects.create_superuser(username="temp_admin")
|
self.temp_admin = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.temp_admin)
|
self.client.force_authenticate(user=self.temp_admin)
|
||||||
|
|
||||||
self.t1 = Tag.objects.create(name="t1")
|
self.t1 = Tag.objects.create(name="t1")
|
||||||
@@ -1276,7 +1274,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
assign_perm("view_tag", self.user3, self.t1)
|
grant_object(self.user3, self.t1, "view_tag")
|
||||||
self.t1.owner = self.user3
|
self.t1.owner = self.user3
|
||||||
self.t1.save()
|
self.t1.save()
|
||||||
|
|
||||||
@@ -1373,13 +1371,9 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
"""
|
"""
|
||||||
self.t1.owner = self.user2
|
self.t1.owner = self.user2
|
||||||
self.t1.save()
|
self.t1.save()
|
||||||
assign_perm("view_tag", self.user1, self.t1)
|
grant_object(self.user1, self.t1, "view_tag")
|
||||||
assign_perm("change_tag", self.user1, self.t1)
|
grant_object(self.user1, self.t1, "change_tag")
|
||||||
self.user1.user_permissions.add(
|
grant_global(self.user1, "view_tag", "change_tag")
|
||||||
*Permission.objects.filter(
|
|
||||||
codename__in=["view_tag", "change_tag"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
user1 = User.objects.get(pk=self.user1.pk)
|
user1 = User.objects.get(pk=self.user1.pk)
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1426,13 +1420,9 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
"""
|
"""
|
||||||
owned = Tag.objects.create(name="owned", owner=self.user1)
|
owned = Tag.objects.create(name="owned", owner=self.user1)
|
||||||
shared = Tag.objects.create(name="shared", owner=self.user2)
|
shared = Tag.objects.create(name="shared", owner=self.user2)
|
||||||
assign_perm("view_tag", self.user1, shared)
|
grant_object(self.user1, shared, "view_tag")
|
||||||
assign_perm("change_tag", self.user1, shared)
|
grant_object(self.user1, shared, "change_tag")
|
||||||
self.user1.user_permissions.add(
|
grant_global(self.user1, "view_tag", "change_tag")
|
||||||
*Permission.objects.filter(
|
|
||||||
codename__in=["view_tag", "change_tag"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
user1 = User.objects.get(pk=self.user1.pk)
|
user1 = User.objects.get(pk=self.user1.pk)
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1473,14 +1463,10 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
"""
|
"""
|
||||||
self.t1.owner = self.user2
|
self.t1.owner = self.user2
|
||||||
self.t1.save()
|
self.t1.save()
|
||||||
assign_perm("view_tag", self.user1, self.t1)
|
grant_object(self.user1, self.t1, "view_tag")
|
||||||
assign_perm("change_tag", self.user1, self.t1)
|
grant_object(self.user1, self.t1, "change_tag")
|
||||||
assign_perm("delete_tag", self.user1, self.t1)
|
grant_object(self.user1, self.t1, "delete_tag")
|
||||||
self.user1.user_permissions.add(
|
grant_global(self.user1, "view_tag", "change_tag", "delete_tag")
|
||||||
*Permission.objects.filter(
|
|
||||||
codename__in=["view_tag", "change_tag", "delete_tag"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
user1 = User.objects.get(pk=self.user1.pk)
|
user1 = User.objects.get(pk=self.user1.pk)
|
||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
@@ -1585,7 +1571,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
- Request succeeds and null is treated as an empty user list,
|
- Request succeeds and null is treated as an empty user list,
|
||||||
so the existing view permission is removed
|
so the existing view permission is removed
|
||||||
"""
|
"""
|
||||||
assign_perm("view_tag", self.user1, self.t1)
|
grant_object(self.user1, self.t1, "view_tag")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_objects/",
|
"/api/bulk_edit_objects/",
|
||||||
@@ -1680,7 +1666,7 @@ class TestFullPermissionsFlag(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.admin = User.objects.create_superuser(username="admin")
|
self.admin = UserFactory(username="admin", superuser=True)
|
||||||
|
|
||||||
def test_full_perms_flag(self) -> None:
|
def test_full_perms_flag(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@@ -7,8 +9,11 @@ import pytest
|
|||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -18,22 +23,14 @@ def consume_file_mock():
|
|||||||
yield m
|
yield m
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def directories(tmp_path, settings, _media_settings):
|
|
||||||
scratch = tmp_path / "scratch"
|
|
||||||
scratch.mkdir()
|
|
||||||
settings.SCRATCH_DIR = scratch
|
|
||||||
return scratch
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestPostDocumentNFCNormalization:
|
class TestPostDocumentNFCNormalization:
|
||||||
def test_nfd_filename_normalized_to_nfc(
|
def test_nfd_filename_normalized_to_nfc(
|
||||||
self,
|
self,
|
||||||
admin_client,
|
admin_client: APIClient,
|
||||||
consume_file_mock: mock.MagicMock,
|
consume_file_mock: mock.MagicMock,
|
||||||
directories,
|
paperless_dirs: PaperlessDirs,
|
||||||
):
|
) -> None:
|
||||||
"""Uploaded file with NFD filename must have its name stored as NFC."""
|
"""Uploaded file with NFD filename must have its name stored as NFC."""
|
||||||
nfd = unicodedata.normalize("NFD", "Rechnung März.pdf")
|
nfd = unicodedata.normalize("NFD", "Rechnung März.pdf")
|
||||||
nfc = unicodedata.normalize("NFC", "Rechnung März.pdf")
|
nfc = unicodedata.normalize("NFC", "Rechnung März.pdf")
|
||||||
@@ -69,10 +66,10 @@ class TestPostDocumentNFCNormalization:
|
|||||||
|
|
||||||
def test_already_nfc_filename_unchanged(
|
def test_already_nfc_filename_unchanged(
|
||||||
self,
|
self,
|
||||||
admin_client,
|
admin_client: APIClient,
|
||||||
consume_file_mock: mock.MagicMock,
|
consume_file_mock: mock.MagicMock,
|
||||||
directories,
|
paperless_dirs: PaperlessDirs,
|
||||||
):
|
) -> None:
|
||||||
"""Uploaded file with already-NFC filename must pass through unchanged."""
|
"""Uploaded file with already-NFC filename must pass through unchanged."""
|
||||||
nfc = unicodedata.normalize("NFC", "Invoice_2024.pdf")
|
nfc = unicodedata.normalize("NFC", "Invoice_2024.pdf")
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from rest_framework import status
|
|||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
# see allauth.socialaccount.providers.openid.provider.OpenIDProvider
|
# see allauth.socialaccount.providers.openid.provider.OpenIDProvider
|
||||||
@@ -55,10 +56,11 @@ class TestApiProfile(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(
|
self.user = UserFactory(
|
||||||
username="temp_admin",
|
username="temp_admin",
|
||||||
first_name="firstname",
|
first_name="firstname",
|
||||||
last_name="surname",
|
last_name="surname",
|
||||||
|
superuser=True,
|
||||||
)
|
)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
@@ -401,7 +403,7 @@ class TestApiTOTPViews(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
def test_get_totp(self) -> None:
|
def test_get_totp(self) -> None:
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import time_machine
|
|||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@@ -27,10 +25,13 @@ from documents.models import Tag
|
|||||||
from documents.models import Workflow
|
from documents.models import Workflow
|
||||||
from documents.search import get_backend
|
from documents.search import get_backend
|
||||||
from documents.search import reset_backend
|
from documents.search import reset_backend
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
pytestmark = pytest.mark.search
|
pytestmark = pytest.mark.search
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
reset_backend()
|
reset_backend()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(username="temp_admin")
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
@@ -949,9 +950,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Terms only within docs user has access to are returned
|
- Terms only within docs user has access to are returned
|
||||||
"""
|
"""
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
u1.user_permissions.add(Permission.objects.get(codename="view_document"))
|
grant_global(u1, "view_document")
|
||||||
|
|
||||||
self.client.force_authenticate(user=u1)
|
self.client.force_authenticate(user=u1)
|
||||||
|
|
||||||
@@ -991,7 +992,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, ["applebaum", "apples"])
|
self.assertEqual(response.data, ["applebaum", "apples"])
|
||||||
|
|
||||||
assign_perm("view_document", u1, d3)
|
grant_object(u1, d3, "view_document")
|
||||||
backend.add_or_update(d3)
|
backend.add_or_update(d3)
|
||||||
|
|
||||||
response = self.client.get("/api/search/autocomplete/?term=app")
|
response = self.client.get("/api/search/autocomplete/?term=app")
|
||||||
@@ -999,10 +1000,10 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.data, ["applebaum", "apples", "appletini"])
|
self.assertEqual(response.data, ["applebaum", "apples", "appletini"])
|
||||||
|
|
||||||
def test_search_autocomplete_group_revocation_is_immediate(self) -> None:
|
def test_search_autocomplete_group_revocation_is_immediate(self) -> None:
|
||||||
user = User.objects.create_user("group-user")
|
user = UserFactory(username="group-user")
|
||||||
owner = User.objects.create_user("document-owner")
|
owner = UserFactory(username="document-owner")
|
||||||
group = Group.objects.create(name="temporary-viewers")
|
group = Group.objects.create(name="temporary-viewers")
|
||||||
user.user_permissions.add(Permission.objects.get(codename="view_document"))
|
grant_global(user, "view_document")
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
|
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
@@ -1011,7 +1012,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
checksum="group-revocation",
|
checksum="group-revocation",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", group, document)
|
grant_object(group, document, "view_document")
|
||||||
get_backend().add_or_update(document)
|
get_backend().add_or_update(document)
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
@@ -1091,11 +1092,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertIsNone(response.data["corrected_query"])
|
self.assertIsNone(response.data["corrected_query"])
|
||||||
|
|
||||||
def test_search_spelling_suggestion_suppressed_for_private_terms(self) -> None:
|
def test_search_spelling_suggestion_suppressed_for_private_terms(self) -> None:
|
||||||
owner = User.objects.create_user("owner")
|
owner = UserFactory(username="owner")
|
||||||
attacker = User.objects.create_user("attacker")
|
attacker = UserFactory(username="attacker")
|
||||||
attacker.user_permissions.add(
|
grant_global(attacker, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = get_backend()
|
backend = get_backend()
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
@@ -1222,11 +1221,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- The request is rejected
|
- The request is rejected
|
||||||
"""
|
"""
|
||||||
owner = User.objects.create_user("owner")
|
owner = UserFactory(username="owner")
|
||||||
attacker = User.objects.create_user("attacker")
|
attacker = UserFactory(username="attacker")
|
||||||
attacker.user_permissions.add(
|
grant_global(attacker, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private_seed = Document.objects.create(
|
private_seed = Document.objects.create(
|
||||||
title="private bank statement",
|
title="private bank statement",
|
||||||
@@ -1534,11 +1531,11 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
- Only owned docs are returned for regular users
|
- Only owned docs are returned for regular users
|
||||||
- All docs are returned for superuser
|
- All docs are returned for superuser
|
||||||
"""
|
"""
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u1, "view_document")
|
||||||
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u2, "view_document")
|
||||||
|
|
||||||
Document.objects.create(checksum="1", content="test 1", owner=u1)
|
Document.objects.create(checksum="1", content="test 1", owner=u1)
|
||||||
Document.objects.create(checksum="2", content="test 2", owner=u2)
|
Document.objects.create(checksum="2", content="test 2", owner=u2)
|
||||||
@@ -1588,10 +1585,10 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Only docs with granted view permissions are returned
|
- Only docs with granted view permissions are returned
|
||||||
"""
|
"""
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u1, "view_document")
|
||||||
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
grant_global(u2, "view_document")
|
||||||
|
|
||||||
d1 = Document.objects.create(checksum="1", content="test 1", owner=u1)
|
d1 = Document.objects.create(checksum="1", content="test 1", owner=u1)
|
||||||
d2 = Document.objects.create(checksum="2", content="test 2", owner=u2)
|
d2 = Document.objects.create(checksum="2", content="test 2", owner=u2)
|
||||||
@@ -1616,9 +1613,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
r = self.client.get("/api/documents/?query=test&owner__isnull=true")
|
r = self.client.get("/api/documents/?query=test&owner__isnull=true")
|
||||||
self.assertEqual(r.data["count"], 1)
|
self.assertEqual(r.data["count"], 1)
|
||||||
|
|
||||||
assign_perm("view_document", u1, d2)
|
grant_object(u1, d2, "view_document")
|
||||||
assign_perm("view_document", u1, d3)
|
grant_object(u1, d3, "view_document")
|
||||||
assign_perm("view_document", u2, d1)
|
grant_object(u2, d1, "view_document")
|
||||||
|
|
||||||
backend.add_or_update(d1)
|
backend.add_or_update(d1)
|
||||||
backend.add_or_update(d2)
|
backend.add_or_update(d2)
|
||||||
@@ -1641,8 +1638,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(r.data["count"], 1)
|
self.assertEqual(r.data["count"], 1)
|
||||||
|
|
||||||
def test_search_sorting(self) -> None:
|
def test_search_sorting(self) -> None:
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
u2 = User.objects.create_user("user2")
|
u2 = UserFactory(username="user2")
|
||||||
c1 = Correspondent.objects.create(name="corres Ax")
|
c1 = Correspondent.objects.create(name="corres Ax")
|
||||||
c2 = Correspondent.objects.create(name="corres Cx")
|
c2 = Correspondent.objects.create(name="corres Cx")
|
||||||
c3 = Correspondent.objects.create(name="corres Bx")
|
c3 = Correspondent.objects.create(name="corres Bx")
|
||||||
@@ -1892,8 +1889,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
pk=5,
|
pk=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1 = User.objects.create_user("bank user1")
|
user1 = UserFactory(username="bank user1")
|
||||||
user2 = User.objects.create_superuser("user2")
|
user2 = UserFactory(username="user2", superuser=True)
|
||||||
group1 = Group.objects.create(name="bank group1")
|
group1 = Group.objects.create(name="bank group1")
|
||||||
Group.objects.create(name="group2")
|
Group.objects.create(name="group2")
|
||||||
|
|
||||||
@@ -1925,7 +1922,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
sort_field="",
|
sort_field="",
|
||||||
owner=user2,
|
owner=user2,
|
||||||
)
|
)
|
||||||
assign_perm("view_savedview", user1, shared_view)
|
grant_object(user1, shared_view, "view_savedview")
|
||||||
mail_account1 = MailAccount.objects.create(name="bank mail account 1")
|
mail_account1 = MailAccount.objects.create(name="bank mail account 1")
|
||||||
mail_account2 = MailAccount.objects.create(name="mail account 2")
|
mail_account2 = MailAccount.objects.create(name="mail account 2")
|
||||||
mail_rule1 = MailRule.objects.create(
|
mail_rule1 = MailRule.objects.create(
|
||||||
@@ -2018,12 +2015,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(returned.get(root.id), "latest content")
|
self.assertEqual(returned.get(root.id), "latest content")
|
||||||
|
|
||||||
def test_global_search_filters_owned_mail_objects(self) -> None:
|
def test_global_search_filters_owned_mail_objects(self) -> None:
|
||||||
user1 = User.objects.create_user("mail-search-user")
|
user1 = UserFactory(username="mail-search-user")
|
||||||
user2 = User.objects.create_user("other-mail-search-user")
|
user2 = UserFactory(username="other-mail-search-user")
|
||||||
user1.user_permissions.add(
|
grant_global(user1, "view_mailaccount", "view_mailrule")
|
||||||
Permission.objects.get(codename="view_mailaccount"),
|
|
||||||
Permission.objects.get(codename="view_mailrule"),
|
|
||||||
)
|
|
||||||
|
|
||||||
own_account = MailAccount.objects.create(
|
own_account = MailAccount.objects.create(
|
||||||
name="bank owned account",
|
name="bank owned account",
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ from datetime import timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -14,8 +12,10 @@ from rest_framework.test import APITestCase
|
|||||||
|
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.permissions import has_system_status_permission
|
from documents.permissions import has_system_status_permission
|
||||||
from documents.tests.factories import PaperlessTaskFactory
|
|
||||||
from paperless import version
|
from paperless import version
|
||||||
|
from paperless_testing.factories import PaperlessTaskFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
|
|
||||||
class TestSystemStatus(APITestCase):
|
class TestSystemStatus(APITestCase):
|
||||||
@@ -23,9 +23,7 @@ class TestSystemStatus(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create_superuser(
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
username="temp_admin",
|
|
||||||
)
|
|
||||||
self.tmp_dir = Path(tempfile.mkdtemp())
|
self.tmp_dir = Path(tempfile.mkdtemp())
|
||||||
self.override = override_settings(MEDIA_ROOT=self.tmp_dir)
|
self.override = override_settings(MEDIA_ROOT=self.tmp_dir)
|
||||||
self.override.enable()
|
self.override.enable()
|
||||||
@@ -96,7 +94,7 @@ class TestSystemStatus(APITestCase):
|
|||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT)
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
self.assertEqual(response["WWW-Authenticate"], "Token")
|
self.assertEqual(response["WWW-Authenticate"], "Token")
|
||||||
normal_user = User.objects.create_user(username="normal_user")
|
normal_user = UserFactory(username="normal_user")
|
||||||
self.client.force_login(normal_user)
|
self.client.force_login(normal_user)
|
||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
@@ -107,10 +105,8 @@ class TestSystemStatus(APITestCase):
|
|||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT)
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
user = User.objects.create_user(username="status_user")
|
user = UserFactory(username="status_user")
|
||||||
user.user_permissions.add(
|
grant_global(user, "view_system_monitoring")
|
||||||
Permission.objects.get(codename="view_system_monitoring"),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
|||||||
@@ -11,22 +11,21 @@ from datetime import timedelta
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from documents.filters import PaperlessTaskFilterSet
|
from documents.filters import PaperlessTaskFilterSet
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from documents.tests.factories import PaperlessTaskFactory
|
from paperless_testing.factories import PaperlessTaskFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
pytestmark = pytest.mark.api
|
pytestmark = pytest.mark.api
|
||||||
|
|
||||||
ENDPOINT = "/api/tasks/"
|
ENDPOINT = "/api/tasks/"
|
||||||
ACCEPT_V10 = "application/json; version=10"
|
|
||||||
ACCEPT_V9 = "application/json; version=9"
|
ACCEPT_V9 = "application/json; version=9"
|
||||||
|
|
||||||
|
|
||||||
@@ -346,21 +345,16 @@ class TestGetTasksV10:
|
|||||||
self,
|
self,
|
||||||
admin_user: User,
|
admin_user: User,
|
||||||
regular_user: User,
|
regular_user: User,
|
||||||
|
user_client: APIClient,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Regular users see their own tasks and unowned (system) tasks; other users' tasks are hidden."""
|
"""Regular users see their own tasks and unowned (system) tasks; other users' tasks are hidden."""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_paperlesstask")
|
||||||
Permission.objects.get(codename="view_paperlesstask"),
|
|
||||||
)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_authenticate(user=regular_user)
|
|
||||||
client.credentials(HTTP_ACCEPT=ACCEPT_V10)
|
|
||||||
|
|
||||||
PaperlessTaskFactory(owner=admin_user) # other user — not visible
|
PaperlessTaskFactory(owner=admin_user) # other user — not visible
|
||||||
unowned_task = PaperlessTaskFactory() # unowned (system task) — visible
|
unowned_task = PaperlessTaskFactory() # unowned (system task) — visible
|
||||||
own_task = PaperlessTaskFactory(owner=regular_user)
|
own_task = PaperlessTaskFactory(owner=regular_user)
|
||||||
|
|
||||||
response = client.get(ENDPOINT)
|
response = user_client.get(ENDPOINT)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 2
|
assert response.data["count"] == 2
|
||||||
@@ -590,9 +584,7 @@ class TestGetTasksV9:
|
|||||||
regular_user: User,
|
regular_user: User,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Non-staff users see their own tasks plus unowned tasks via v9 API."""
|
"""Non-staff users see their own tasks plus unowned tasks via v9 API."""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_paperlesstask")
|
||||||
Permission.objects.get(codename="view_paperlesstask"),
|
|
||||||
)
|
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_authenticate(user=regular_user)
|
client.force_authenticate(user=regular_user)
|
||||||
@@ -732,19 +724,17 @@ class TestAcknowledge:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
def test_succeeds_with_change_permission(self, regular_user: User) -> None:
|
def test_succeeds_with_change_permission(
|
||||||
|
self,
|
||||||
|
regular_user: User,
|
||||||
|
user_client: APIClient,
|
||||||
|
) -> None:
|
||||||
"""Users granted change_paperlesstask permission can acknowledge tasks."""
|
"""Users granted change_paperlesstask permission can acknowledge tasks."""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "change_paperlesstask")
|
||||||
Permission.objects.get(codename="change_paperlesstask"),
|
|
||||||
)
|
|
||||||
regular_user.save()
|
regular_user.save()
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_authenticate(user=regular_user)
|
|
||||||
client.credentials(HTTP_ACCEPT=ACCEPT_V10)
|
|
||||||
|
|
||||||
task = PaperlessTaskFactory()
|
task = PaperlessTaskFactory()
|
||||||
response = client.post(
|
response = user_client.post(
|
||||||
ENDPOINT + "acknowledge/",
|
ENDPOINT + "acknowledge/",
|
||||||
{"tasks": [task.id]},
|
{"tasks": [task.id]},
|
||||||
format="json",
|
format="json",
|
||||||
@@ -807,9 +797,7 @@ class TestSummaryPermissions:
|
|||||||
regular_user,
|
regular_user,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""A user with view_system_monitoring but no document permissions can access summary/."""
|
"""A user with view_system_monitoring but no document permissions can access summary/."""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_system_monitoring")
|
||||||
Permission.objects.get(codename="view_system_monitoring"),
|
|
||||||
)
|
|
||||||
|
|
||||||
response = user_client.get(ENDPOINT + "summary/")
|
response = user_client.get(ENDPOINT + "summary/")
|
||||||
|
|
||||||
@@ -822,9 +810,7 @@ class TestSummaryPermissions:
|
|||||||
admin_user,
|
admin_user,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Monitoring user sees aggregate data for all tasks, not just unowned ones."""
|
"""Monitoring user sees aggregate data for all tasks, not just unowned ones."""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_system_monitoring")
|
||||||
Permission.objects.get(codename="view_system_monitoring"),
|
|
||||||
)
|
|
||||||
PaperlessTaskFactory(
|
PaperlessTaskFactory(
|
||||||
owner=admin_user,
|
owner=admin_user,
|
||||||
task_type=PaperlessTask.TaskType.CONSUME_FILE,
|
task_type=PaperlessTask.TaskType.CONSUME_FILE,
|
||||||
@@ -845,9 +831,7 @@ class TestSummaryPermissions:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""A regular user with view_paperlesstask but not view_system_monitoring sees only
|
"""A regular user with view_paperlesstask but not view_system_monitoring sees only
|
||||||
their own tasks and unowned tasks in the summary, not other users' tasks."""
|
their own tasks and unowned tasks in the summary, not other users' tasks."""
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_paperlesstask")
|
||||||
Permission.objects.get(codename="view_paperlesstask"),
|
|
||||||
)
|
|
||||||
|
|
||||||
PaperlessTaskFactory(
|
PaperlessTaskFactory(
|
||||||
owner=regular_user,
|
owner=regular_user,
|
||||||
@@ -1012,9 +996,7 @@ class TestDuplicateDocumentsPermissions:
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_v9_client(self, regular_user: User) -> APIClient:
|
def user_v9_client(self, regular_user: User) -> APIClient:
|
||||||
regular_user.user_permissions.add(
|
grant_global(regular_user, "view_paperlesstask")
|
||||||
Permission.objects.get(codename="view_paperlesstask"),
|
|
||||||
)
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_authenticate(user=regular_user)
|
client.force_authenticate(user=regular_user)
|
||||||
client.credentials(HTTP_ACCEPT=ACCEPT_V9)
|
client.credentials(HTTP_ACCEPT=ACCEPT_V9)
|
||||||
@@ -1085,7 +1067,7 @@ class TestDuplicateDocumentsPermissions:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""A user with explicit guardian view_document permission sees the duplicate_of document."""
|
"""A user with explicit guardian view_document permission sees the duplicate_of document."""
|
||||||
doc = DocumentFactory(owner=admin_user, title="Granted Doc")
|
doc = DocumentFactory(owner=admin_user, title="Granted Doc")
|
||||||
assign_perm("view_document", regular_user, doc)
|
grant_object(regular_user, doc, "view_document")
|
||||||
PaperlessTaskFactory(
|
PaperlessTaskFactory(
|
||||||
owner=regular_user,
|
owner=regular_user,
|
||||||
status=PaperlessTask.Status.SUCCESS,
|
status=PaperlessTask.Status.SUCCESS,
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_all_global
|
||||||
|
|
||||||
|
|
||||||
class TestTrashAPI(DirectoriesMixin, APITestCase):
|
class TestTrashAPI(DirectoriesMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_user(username="temp_admin")
|
self.user = UserFactory(username="temp_admin")
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
grant_all_global(self.user)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(Document.global_objects.count(), 0)
|
self.assertEqual(Document.global_objects.count(), 0)
|
||||||
|
|
||||||
def test_trash_list_requires_global_document_view_permission(self) -> None:
|
def test_trash_list_requires_global_document_view_permission(self) -> None:
|
||||||
user = User.objects.create_user(username="trash_owner")
|
user = UserFactory(username="trash_owner")
|
||||||
document = Document.objects.create(title="Owned", owner=user)
|
document = Document.objects.create(title="Owned", owner=user)
|
||||||
document.delete()
|
document.delete()
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
@@ -140,7 +140,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
|
|||||||
created=date(2023, 1, 2),
|
created=date(2023, 1, 2),
|
||||||
)
|
)
|
||||||
document_not_owned.delete()
|
document_not_owned.delete()
|
||||||
user2 = User.objects.create_user(username="user2")
|
user2 = UserFactory(username="user2")
|
||||||
document_u2 = Document.objects.create(
|
document_u2 = Document.objects.create(
|
||||||
title="Title3",
|
title="Title3",
|
||||||
content="content3",
|
content="content3",
|
||||||
@@ -158,7 +158,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(resp.data["results"][1]["id"], document_u1.pk)
|
self.assertEqual(resp.data["results"][1]["id"], document_u1.pk)
|
||||||
|
|
||||||
# superuser sees all documents
|
# superuser sees all documents
|
||||||
superuser = User.objects.create_superuser(username="superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
resp = self.client.get("/api/trash/")
|
resp = self.client.get("/api/trash/")
|
||||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
@@ -174,7 +174,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
|
|||||||
- 403 Forbidden
|
- 403 Forbidden
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user2 = User.objects.create_user(username="user2")
|
user2 = UserFactory(username="user2")
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
title="Title",
|
title="Title",
|
||||||
content="content",
|
content="content",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from paperless.version import __full_version_str__
|
from paperless.version import __full_version_str__
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||||
@@ -15,7 +15,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.test_user = User.objects.create_superuser(username="test")
|
self.test_user = UserFactory(username="test", superuser=True)
|
||||||
self.test_user.first_name = "Test"
|
self.test_user.first_name = "Test"
|
||||||
self.test_user.last_name = "User"
|
self.test_user.last_name = "User"
|
||||||
self.test_user.save()
|
self.test_user.save()
|
||||||
@@ -91,7 +91,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_api_set_ui_settings_insufficient_global_permissions(self) -> None:
|
def test_api_set_ui_settings_insufficient_global_permissions(self) -> None:
|
||||||
not_superuser = User.objects.create_user(username="test_not_superuser")
|
not_superuser = UserFactory(username="test_not_superuser")
|
||||||
self.client.force_authenticate(user=not_superuser)
|
self.client.force_authenticate(user=not_superuser)
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
@@ -111,7 +111,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_api_set_ui_settings_sufficient_global_permissions(self) -> None:
|
def test_api_set_ui_settings_sufficient_global_permissions(self) -> None:
|
||||||
not_superuser = User.objects.create_user(username="test_not_superuser")
|
not_superuser = UserFactory(username="test_not_superuser")
|
||||||
not_superuser.user_permissions.add(
|
not_superuser.user_permissions.add(
|
||||||
*Permission.objects.filter(codename__contains="uisettings"),
|
*Permission.objects.filter(codename__contains="uisettings"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from documents.models import Tag
|
|||||||
from documents.models import Workflow
|
from documents.models import Workflow
|
||||||
from documents.models import WorkflowAction
|
from documents.models import WorkflowAction
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||||
@@ -25,7 +26,7 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
user = User.objects.create_superuser(username="temp_admin")
|
user = UserFactory(username="temp_admin", superuser=True)
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=user)
|
||||||
self.user2 = User.objects.create(username="user2")
|
self.user2 = User.objects.create(username="user2")
|
||||||
self.user3 = User.objects.create(username="user3")
|
self.user3 = User.objects.create(username="user3")
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ from documents.models import Document
|
|||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.plugins.base import StopConsumeTaskError
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
from documents.tests.utils import ConsumeTaskMixin
|
from documents.tests.utils import ConsumeTaskMixin
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import DummyProgressManager
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
class GetReaderPluginMixin:
|
class GetReaderPluginMixin:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from django.contrib.auth.models import User
|
|||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from guardian.shortcuts import get_groups_with_perms
|
from guardian.shortcuts import get_groups_with_perms
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
|
|
||||||
@@ -23,7 +22,8 @@ from documents.models import DocumentType
|
|||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.permissions import set_permissions_for_objects
|
from documents.permissions import set_permissions_for_objects
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestBulkEdit(DirectoriesMixin, TestCase):
|
class TestBulkEdit(DirectoriesMixin, TestCase):
|
||||||
@@ -440,7 +440,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
|
|||||||
def test_set_permissions(self, m) -> None:
|
def test_set_permissions(self, m) -> None:
|
||||||
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
|
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
|
||||||
|
|
||||||
assign_perm("view_document", self.group1, self.doc1)
|
grant_object(self.group1, self.doc1, "view_document")
|
||||||
|
|
||||||
permissions = {
|
permissions = {
|
||||||
"view": {
|
"view": {
|
||||||
@@ -482,8 +482,8 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
|
|||||||
self.doc1.owner = self.user1
|
self.doc1.owner = self.user1
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
|
|
||||||
assign_perm("view_document", self.user1, self.doc1)
|
grant_object(self.user1, self.doc1, "view_document")
|
||||||
assign_perm("view_document", self.group1, self.doc1)
|
grant_object(self.group1, self.doc1, "view_document")
|
||||||
|
|
||||||
permissions = {
|
permissions = {
|
||||||
"view": {
|
"view": {
|
||||||
@@ -609,7 +609,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
|
|||||||
self.doc1.owner = self.user1
|
self.doc1.owner = self.user1
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
self.user1.groups.add(self.group1)
|
self.user1.groups.add(self.group1)
|
||||||
assign_perm("view_document", self.group1, self.doc1)
|
grant_object(self.group1, self.doc1, "view_document")
|
||||||
|
|
||||||
bulk_edit.set_permissions(
|
bulk_edit.set_permissions(
|
||||||
[self.doc1.id],
|
[self.doc1.id],
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ from documents.models import DocumentType
|
|||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.factories import TagFactory
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from paperless.settings import CLASSIFIER_LANGUAGES
|
from paperless.settings import CLASSIFIER_LANGUAGES
|
||||||
from paperless.signed_pickle import HMAC_SIZE
|
from paperless.signed_pickle import HMAC_SIZE
|
||||||
from paperless.signed_pickle import signed_pickle_dumps
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import TagFactory
|
||||||
|
|
||||||
|
|
||||||
def dummy_preprocess(content: str) -> str:
|
def dummy_preprocess(content: str) -> str:
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ from documents.models import Tag
|
|||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
from documents.plugins.helpers import ProgressStatusOptions
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.tasks import sanity_check
|
from documents.tasks import sanity_check
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import DummyProgressManager
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import GetConsumerMixin
|
from documents.tests.utils import GetConsumerMixin
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class _BaseNewStyleParser:
|
class _BaseNewStyleParser:
|
||||||
@@ -769,7 +770,7 @@ class TestConsumer(
|
|||||||
|
|
||||||
original_modified = timezone.now() - datetime.timedelta(days=1)
|
original_modified = timezone.now() - datetime.timedelta(days=1)
|
||||||
Document.objects.filter(pk=root_doc.pk).update(modified=original_modified)
|
Document.objects.filter(pk=root_doc.pk).update(modified=original_modified)
|
||||||
actor = User.objects.create_user(
|
actor = UserFactory(
|
||||||
username="actor",
|
username="actor",
|
||||||
email="actor@example.com",
|
email="actor@example.com",
|
||||||
password="password",
|
password="password",
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ from django.test.utils import CaptureQueriesContext
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.versioning import LATEST_VERSION_CONTENT_PREFETCH_ATTR
|
from documents.versioning import LATEST_VERSION_CONTENT_PREFETCH_ATTR
|
||||||
from documents.versioning import has_prefetched_effective_content
|
from documents.versioning import has_prefetched_effective_content
|
||||||
from documents.versioning import latest_version_content_prefetch
|
from documents.versioning import latest_version_content_prefetch
|
||||||
from documents.views import DocumentViewSet
|
from documents.views import DocumentViewSet
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from documents.data_models import ConsumableDocument
|
|||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.double_sided import STAGING_FILE_NAME
|
from documents.double_sided import STAGING_FILE_NAME
|
||||||
from documents.double_sided import TIMEOUT_MINUTES
|
from documents.double_sided import TIMEOUT_MINUTES
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import DummyProgressManager
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@@ -29,8 +29,8 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.dirs.double_sided_dir = self.dirs.consumption_dir / "double-sided"
|
self.double_sided_dir = self.dirs.consumption_dir / "double-sided"
|
||||||
self.dirs.double_sided_dir.mkdir()
|
self.double_sided_dir.mkdir()
|
||||||
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
|
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
|
||||||
|
|
||||||
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
|
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
|
||||||
@@ -39,7 +39,7 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
destination file does not exist afterwards
|
destination file does not exist afterwards
|
||||||
"""
|
"""
|
||||||
src = self.SAMPLE_DIR / srcname
|
src = self.SAMPLE_DIR / srcname
|
||||||
dst = self.dirs.double_sided_dir / dstname
|
dst = self.double_sided_dir / dstname
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
@@ -214,31 +214,41 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
self.assertIsNotFile(self.staging_file)
|
self.assertIsNotFile(self.staging_file)
|
||||||
self.assertIsInstance(msg.get("document_id"), int)
|
self.assertIsInstance(msg.get("document_id"), int)
|
||||||
|
|
||||||
def test_subdirectory_upload(self) -> None:
|
def test_consume_double_sided_in_nested_dir(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- A staging file exists
|
- A staging file exists
|
||||||
WHEN:
|
WHEN:
|
||||||
- A file gets uploaded into foo/bar/double-sided
|
- A file is uploaded into foo/bar/double-sided
|
||||||
or double-sided/foo/bar
|
|
||||||
THEN:
|
THEN:
|
||||||
- The collated file gets put into foo/bar
|
- The collated file is placed into foo/bar
|
||||||
"""
|
"""
|
||||||
# TODO: parameterize this instead
|
self.create_staging_file()
|
||||||
for path in [
|
self.consume_file(
|
||||||
Path("foo") / "bar" / "double-sided",
|
"double-sided-odd.pdf",
|
||||||
Path("double-sided") / "foo" / "bar",
|
Path("foo") / "bar" / "double-sided" / "foo.pdf",
|
||||||
]:
|
)
|
||||||
with self.subTest(path=str(path)):
|
self.assertIsFile(
|
||||||
# Ensure we get fresh directories for each run
|
self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf",
|
||||||
self.tearDown()
|
)
|
||||||
self.setUp()
|
|
||||||
|
|
||||||
self.create_staging_file()
|
def test_consume_double_sided_with_nested_subdir(self) -> None:
|
||||||
self.consume_file("double-sided-odd.pdf", Path(path) / "foo.pdf")
|
"""
|
||||||
self.assertIsFile(
|
GIVEN:
|
||||||
self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf",
|
- A staging file exists
|
||||||
)
|
WHEN:
|
||||||
|
- A file is uploaded into double-sided/foo/bar
|
||||||
|
THEN:
|
||||||
|
- The collated file is placed into foo/bar
|
||||||
|
"""
|
||||||
|
self.create_staging_file()
|
||||||
|
self.consume_file(
|
||||||
|
"double-sided-odd.pdf",
|
||||||
|
Path("double-sided") / "foo" / "bar" / "foo.pdf",
|
||||||
|
)
|
||||||
|
self.assertIsFile(
|
||||||
|
self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
@override_settings(CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=False)
|
@override_settings(CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=False)
|
||||||
def test_disabled_double_sided_dir_upload(self) -> None:
|
def test_disabled_double_sided_dir_upload(self) -> None:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from unittest import mock
|
|||||||
import pytest
|
import pytest
|
||||||
from auditlog.context import disable_auditlog
|
from auditlog.context import disable_auditlog
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import DatabaseError
|
from django.db import DatabaseError
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -30,9 +29,10 @@ from documents.models import DocumentType
|
|||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.serialisers import DocumentSerializer
|
from documents.serialisers import DocumentSerializer
|
||||||
from documents.tasks import empty_trash
|
from documents.tasks import empty_trash
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
@@ -1323,7 +1323,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
- Document without owner returns "none"
|
- Document without owner returns "none"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
u1 = User.objects.create_user("user1")
|
u1 = UserFactory(username="user1")
|
||||||
|
|
||||||
owned_doc = Document.objects.create(
|
owned_doc = Document.objects.create(
|
||||||
title="The Title",
|
title="The Title",
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import pytest
|
|||||||
from documents.file_handling import generate_filename
|
from documents.file_handling import generate_filename
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.tests.factories import CorrespondentFactory
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from documents.tests.factories import StoragePathFactory
|
from paperless_testing.factories import StoragePathFactory
|
||||||
from documents.tests.factories import TagFactory
|
from paperless_testing.factories import TagFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ if TYPE_CHECKING:
|
|||||||
from documents.file_handling import generate_filename
|
from documents.file_handling import generate_filename
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tasks import update_document_content_maybe_archive_file
|
from documents.tasks import update_document_content_maybe_archive_file
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
|
sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ from django.test import override_settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.models import GroupObjectPermission
|
from guardian.models import GroupObjectPermission
|
||||||
from guardian.models import UserObjectPermission
|
from guardian.models import UserObjectPermission
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
|
|
||||||
from documents.management.commands import document_exporter
|
from documents.management.commands import document_exporter
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
@@ -46,11 +45,12 @@ from documents.models import WorkflowTrigger
|
|||||||
from documents.sanity_checker import check_sanity
|
from documents.sanity_checker import check_sanity
|
||||||
from documents.settings import EXPORTER_FILE_NAME
|
from documents.settings import EXPORTER_FILE_NAME
|
||||||
from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME
|
from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
from documents.tests.utils import paperless_environment
|
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.dirs import paperless_environment
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
@@ -105,8 +105,8 @@ class TestExportImport(
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("view_document", self.user2, self.d2)
|
grant_object(self.user2, self.d2, "view_document")
|
||||||
assign_perm("view_document", self.group1, self.d3)
|
grant_object(self.group1, self.d3, "view_document")
|
||||||
|
|
||||||
self.t1 = Tag.objects.create(name="t")
|
self.t1 = Tag.objects.create(name="t")
|
||||||
self.dt1 = DocumentType.objects.create(name="dt")
|
self.dt1 = DocumentType.objects.create(name="dt")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.core.management import call_command
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from documents.management.commands.document_importer import _deserialize_record
|
|||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.settings import EXPORTER_ARCHIVE_NAME
|
from documents.settings import EXPORTER_ARCHIVE_NAME
|
||||||
from documents.settings import EXPORTER_FILE_NAME
|
from documents.settings import EXPORTER_FILE_NAME
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ from documents.models import DocumentType
|
|||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.factories import CorrespondentFactory
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
from documents.tests.factories import DocumentTypeFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from documents.tests.factories import StoragePathFactory
|
from paperless_testing.factories import DocumentTypeFactory
|
||||||
from documents.tests.factories import TagFactory
|
from paperless_testing.factories import StoragePathFactory
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.factories import TagFactory
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Module-level type aliases
|
# Module-level type aliases
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from django.test import TestCase
|
|||||||
from documents.management.commands.document_thumbnails import _process_document
|
from documents.management.commands.document_thumbnails import _process_document
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.parsers import get_default_thumbnail
|
from documents.parsers import get_default_thumbnail
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from documents import matching
|
|||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from documents.signals import document_consumption_finished
|
from documents.signals import document_consumption_finished
|
||||||
from documents.tests.factories import CorrespondentFactory
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from documents.tests.factories import DocumentTypeFactory
|
from paperless_testing.factories import DocumentTypeFactory
|
||||||
from documents.tests.factories import TagFactory
|
from paperless_testing.factories import TagFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import json
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -12,6 +10,8 @@ from rest_framework.test import APITestCase
|
|||||||
from documents.bulk_edit import merge_as_versions
|
from documents.bulk_edit import merge_as_versions
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.serialisers import MergeDocumentsAsVersionsSerializer
|
from documents.serialisers import MergeDocumentsAsVersionsSerializer
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
|
|
||||||
class TestMergeDocumentsAsVersionsSerializer(TestCase):
|
class TestMergeDocumentsAsVersionsSerializer(TestCase):
|
||||||
@@ -294,7 +294,7 @@ class TestMergeDocumentsAsVersions(TestCase):
|
|||||||
@mock.patch("documents.bulk_edit.bulk_update_documents.apply_async")
|
@mock.patch("documents.bulk_edit.bulk_update_documents.apply_async")
|
||||||
@mock.patch("documents.search.get_backend")
|
@mock.patch("documents.search.get_backend")
|
||||||
def test_writes_audit_log_entry(self, *_mocks) -> None:
|
def test_writes_audit_log_entry(self, *_mocks) -> None:
|
||||||
user = User.objects.create_user(username="merger")
|
user = UserFactory(username="merger")
|
||||||
root = Document.objects.create(checksum="A", title="Root")
|
root = Document.objects.create(checksum="A", title="Root")
|
||||||
source = Document.objects.create(checksum="B", title="Source")
|
source = Document.objects.create(checksum="B", title="Source")
|
||||||
LogEntry.objects.all().delete()
|
LogEntry.objects.all().delete()
|
||||||
@@ -335,12 +335,8 @@ class TestMergeDocumentsAsVersions(TestCase):
|
|||||||
|
|
||||||
class TestMergeDocumentsAsVersionsAPI(APITestCase):
|
class TestMergeDocumentsAsVersionsAPI(APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.create_user(username="user")
|
self.user = UserFactory(username="user")
|
||||||
self.user.user_permissions.add(
|
grant_global(self.user, "change_document", "view_document", "delete_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
Permission.objects.get(codename="delete_document"),
|
|
||||||
)
|
|
||||||
self.doc1 = Document.objects.create(
|
self.doc1 = Document.objects.create(
|
||||||
checksum="A",
|
checksum="A",
|
||||||
title="A",
|
title="A",
|
||||||
@@ -382,7 +378,7 @@ class TestMergeDocumentsAsVersionsAPI(APITestCase):
|
|||||||
@mock.patch("documents.views.bulk_edit.merge_as_versions")
|
@mock.patch("documents.views.bulk_edit.merge_as_versions")
|
||||||
def test_requires_change_permission(self, merge_mock) -> None:
|
def test_requires_change_permission(self, merge_mock) -> None:
|
||||||
merge_mock.__name__ = "merge_as_versions"
|
merge_mock.__name__ = "merge_as_versions"
|
||||||
user = User.objects.create_user(username="no-change")
|
user = UserFactory(username="no-change")
|
||||||
self.doc1.owner = user
|
self.doc1.owner = user
|
||||||
self.doc1.save()
|
self.doc1.save()
|
||||||
self.doc2.owner = user
|
self.doc2.owner = user
|
||||||
@@ -405,11 +401,8 @@ class TestMergeDocumentsAsVersionsAPI(APITestCase):
|
|||||||
def test_requires_delete_permission(self, merge_mock) -> None:
|
def test_requires_delete_permission(self, merge_mock) -> None:
|
||||||
merge_mock.__name__ = "merge_as_versions"
|
merge_mock.__name__ = "merge_as_versions"
|
||||||
# Owns them and may change them, but may not make them stop being documents
|
# Owns them and may change them, but may not make them stop being documents
|
||||||
user = User.objects.create_user(username="no-delete")
|
user = UserFactory(username="no-delete")
|
||||||
user.user_permissions.add(
|
grant_global(user, "change_document", "view_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
for doc in (self.doc1, self.doc2):
|
for doc in (self.doc1, self.doc2):
|
||||||
doc.owner = user
|
doc.owner = user
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import shutil
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -19,19 +18,22 @@ class TestSha256ChecksumDataMigration(TestMigrations):
|
|||||||
|
|
||||||
migrate_from = "0015_document_version_index_and_more"
|
migrate_from = "0015_document_version_index_and_more"
|
||||||
migrate_to = "0016_sha256_checksums"
|
migrate_to = "0016_sha256_checksums"
|
||||||
|
migrate_once = True
|
||||||
reset_sequences = True
|
reset_sequences = True
|
||||||
|
|
||||||
ORIGINAL_CONTENT = b"original file content for sha256 migration test"
|
ORIGINAL_CONTENT = b"original file content for sha256 migration test"
|
||||||
ARCHIVE_CONTENT = b"archive file content for sha256 migration test"
|
ARCHIVE_CONTENT = b"archive file content for sha256 migration test"
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps) -> None:
|
@classmethod
|
||||||
self._originals_dir = Path(tempfile.mkdtemp())
|
def setUpClass(cls) -> None:
|
||||||
self._archive_dir = Path(tempfile.mkdtemp())
|
super().setUpClass()
|
||||||
self._settings_override = override_settings(
|
originals_dir = Path(cls.enterClassContext(tempfile.TemporaryDirectory()))
|
||||||
ORIGINALS_DIR=self._originals_dir,
|
archive_dir = Path(cls.enterClassContext(tempfile.TemporaryDirectory()))
|
||||||
ARCHIVE_DIR=self._archive_dir,
|
cls.enterClassContext(
|
||||||
|
override_settings(ORIGINALS_DIR=originals_dir, ARCHIVE_DIR=archive_dir),
|
||||||
)
|
)
|
||||||
self._settings_override.enable()
|
|
||||||
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
Document = apps.get_model("documents", "Document")
|
Document = apps.get_model("documents", "Document")
|
||||||
|
|
||||||
# doc1: original file present, no archive
|
# doc1: original file present, no archive
|
||||||
@@ -85,8 +87,9 @@ class TestSha256ChecksumDataMigration(TestMigrations):
|
|||||||
archive_checksum=None,
|
archive_checksum=None,
|
||||||
).pk
|
).pk
|
||||||
|
|
||||||
def _fixture_teardown(self) -> None:
|
@classmethod
|
||||||
super()._fixture_teardown()
|
def tearDownClass(cls) -> None:
|
||||||
|
super().tearDownClass()
|
||||||
# Django's SQLite backend returns [] from sequence_reset_sql(), so
|
# Django's SQLite backend returns [] from sequence_reset_sql(), so
|
||||||
# reset_sequences=True flushes rows but never clears sqlite_sequence.
|
# reset_sequences=True flushes rows but never clears sqlite_sequence.
|
||||||
# Explicitly delete the entry so subsequent tests start from pk=1.
|
# Explicitly delete the entry so subsequent tests start from pk=1.
|
||||||
@@ -96,12 +99,6 @@ class TestSha256ChecksumDataMigration(TestMigrations):
|
|||||||
"DELETE FROM sqlite_sequence WHERE name='documents_document'",
|
"DELETE FROM sqlite_sequence WHERE name='documents_document'",
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
self._settings_override.disable()
|
|
||||||
shutil.rmtree(self._originals_dir, ignore_errors=True)
|
|
||||||
shutil.rmtree(self._archive_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def test_original_checksum_updated_to_sha256_when_file_exists(self) -> None:
|
def test_original_checksum_updated_to_sha256_when_file_exists(self) -> None:
|
||||||
Document = self.apps.get_model("documents", "Document")
|
Document = self.apps.get_model("documents", "Document")
|
||||||
doc = Document.objects.get(pk=self.doc1_id)
|
doc = Document.objects.get(pk=self.doc1_id)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import pytest
|
|||||||
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.factories import CorrespondentFactory
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from documents.matching import match_correspondents
|
from documents.matching import match_correspondents
|
||||||
@@ -24,11 +22,17 @@ from documents.permissions import permitted_document_ids
|
|||||||
from documents.permissions import permitted_object_ids
|
from documents.permissions import permitted_object_ids
|
||||||
from documents.permissions import restrict_queryset_to_visible
|
from documents.permissions import restrict_queryset_to_visible
|
||||||
from documents.serialisers import _get_viewable_duplicates
|
from documents.serialisers import _get_viewable_duplicates
|
||||||
from documents.tests.factories import CorrespondentFactory
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from documents.tests.factories import DocumentTypeFactory
|
from paperless_testing.factories import DocumentTypeFactory
|
||||||
from documents.tests.factories import StoragePathFactory
|
from paperless_testing.factories import StoragePathFactory
|
||||||
from documents.tests.factories import TagFactory
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
|
|
||||||
def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden):
|
def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden):
|
||||||
@@ -47,8 +51,8 @@ def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestPermittedDocumentIdsSecurity:
|
class TestPermittedDocumentIdsSecurity:
|
||||||
def test_owner_sees_own_document(self):
|
def test_owner_sees_own_document(self):
|
||||||
user = User.objects.create_user(username="alice")
|
user = UserFactory(username="alice")
|
||||||
stranger = User.objects.create_user(username="mallory")
|
stranger = UserFactory(username="mallory")
|
||||||
owned = DocumentFactory(owner=user)
|
owned = DocumentFactory(owner=user)
|
||||||
strangers_doc = DocumentFactory(owner=stranger)
|
strangers_doc = DocumentFactory(owner=stranger)
|
||||||
|
|
||||||
@@ -61,7 +65,7 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_unowned_document_visible_to_everyone(self):
|
def test_unowned_document_visible_to_everyone(self):
|
||||||
user = User.objects.create_user(username="alice")
|
user = UserFactory(username="alice")
|
||||||
unowned = DocumentFactory(owner=None)
|
unowned = DocumentFactory(owner=None)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
@@ -71,12 +75,12 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_explicit_user_permission_grants_visibility(self):
|
def test_explicit_user_permission_grants_visibility(self):
|
||||||
grantee = User.objects.create_user(username="alice")
|
grantee = UserFactory(username="alice")
|
||||||
stranger = User.objects.create_user(username="mallory")
|
stranger = UserFactory(username="mallory")
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
shared = DocumentFactory(owner=owner)
|
shared = DocumentFactory(owner=owner)
|
||||||
not_shared = DocumentFactory(owner=owner)
|
not_shared = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", grantee, shared)
|
grant_object(grantee, shared, "view_document")
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_document_ids(grantee),
|
permitted_document_ids(grantee),
|
||||||
@@ -90,13 +94,13 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_explicit_group_permission_grants_visibility_to_members_only(self):
|
def test_explicit_group_permission_grants_visibility_to_members_only(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
member = User.objects.create_user(username="member")
|
member = UserFactory(username="member")
|
||||||
non_member = User.objects.create_user(username="non_member")
|
non_member = UserFactory(username="non_member")
|
||||||
group = Group.objects.create(name="finance")
|
group = Group.objects.create(name="finance")
|
||||||
member.groups.add(group)
|
member.groups.add(group)
|
||||||
shared = DocumentFactory(owner=owner)
|
shared = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", group, shared)
|
grant_object(group, shared, "view_document")
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_document_ids(member),
|
permitted_document_ids(member),
|
||||||
@@ -110,7 +114,7 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_soft_deleted_document_excluded_by_default(self):
|
def test_soft_deleted_document_excluded_by_default(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
doc.delete() # soft delete
|
doc.delete() # soft delete
|
||||||
doc.refresh_from_db()
|
doc.refresh_from_db()
|
||||||
@@ -126,8 +130,8 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_superuser_sees_everything_including_no_perm_documents(self):
|
def test_superuser_sees_everything_including_no_perm_documents(self):
|
||||||
superuser = User.objects.create_superuser(username="root")
|
superuser = UserFactory(username="root", superuser=True)
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
@@ -137,7 +141,7 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_anonymous_user_sees_only_unowned_documents(self):
|
def test_anonymous_user_sees_only_unowned_documents(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
owned = DocumentFactory(owner=owner)
|
owned = DocumentFactory(owner=owner)
|
||||||
unowned = DocumentFactory(owner=None)
|
unowned = DocumentFactory(owner=None)
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ class TestPermittedDocumentIdsSecurity:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestPermittedDocumentIdsIncludeDeleted:
|
class TestPermittedDocumentIdsIncludeDeleted:
|
||||||
def test_include_deleted_true_reveals_soft_deleted_owned_document(self):
|
def test_include_deleted_true_reveals_soft_deleted_owned_document(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
@@ -162,8 +166,8 @@ class TestPermittedDocumentIdsIncludeDeleted:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_include_deleted_true_still_respects_permission_boundary(self):
|
def test_include_deleted_true_still_respects_permission_boundary(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
stranger = User.objects.create_user(username="mallory")
|
stranger = UserFactory(username="mallory")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
@@ -191,14 +195,12 @@ class TestAiChatAllDocumentsPermissionBoundary:
|
|||||||
def test_chat_all_documents_excludes_unshared_document(self, mock_stream_chat):
|
def test_chat_all_documents_excludes_unshared_document(self, mock_stream_chat):
|
||||||
mock_stream_chat.return_value = iter([b"data"])
|
mock_stream_chat.return_value = iter([b"data"])
|
||||||
|
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
asker = User.objects.create_user(username="asker")
|
asker = UserFactory(username="asker")
|
||||||
asker.user_permissions.add(
|
grant_global(asker, "view_document")
|
||||||
*Permission.objects.filter(codename="view_document"),
|
|
||||||
)
|
|
||||||
shared = DocumentFactory(owner=owner)
|
shared = DocumentFactory(owner=owner)
|
||||||
not_shared = DocumentFactory(owner=owner)
|
not_shared = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", asker, shared)
|
grant_object(asker, shared, "view_document")
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_authenticate(user=asker)
|
client.force_authenticate(user=asker)
|
||||||
@@ -219,13 +221,13 @@ class TestAiChatAllDocumentsPermissionBoundary:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestDuplicateDocumentsPermissionBoundary:
|
class TestDuplicateDocumentsPermissionBoundary:
|
||||||
def test_get_viewable_duplicates_includes_soft_deleted_but_respects_perms(self):
|
def test_get_viewable_duplicates_includes_soft_deleted_but_respects_perms(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
stranger = User.objects.create_user(username="mallory")
|
stranger = UserFactory(username="mallory")
|
||||||
original = DocumentFactory(owner=owner, checksum="dupe-checksum")
|
original = DocumentFactory(owner=owner, checksum="dupe-checksum")
|
||||||
dup_visible = DocumentFactory(owner=owner, checksum="dupe-checksum")
|
dup_visible = DocumentFactory(owner=owner, checksum="dupe-checksum")
|
||||||
dup_hidden = DocumentFactory(owner=owner, checksum="dupe-checksum")
|
dup_hidden = DocumentFactory(owner=owner, checksum="dupe-checksum")
|
||||||
dup_hidden.delete() # soft delete, should still be found (include_deleted=True)
|
dup_hidden.delete() # soft delete, should still be found (include_deleted=True)
|
||||||
assign_perm("view_document", stranger, dup_visible)
|
grant_object(stranger, dup_visible, "view_document")
|
||||||
|
|
||||||
result_owner = _get_viewable_duplicates(original, owner)
|
result_owner = _get_viewable_duplicates(original, owner)
|
||||||
assert {d.pk for d in result_owner} == {dup_visible.pk, dup_hidden.pk}
|
assert {d.pk for d in result_owner} == {dup_visible.pk, dup_hidden.pk}
|
||||||
@@ -237,13 +239,13 @@ class TestDuplicateDocumentsPermissionBoundary:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestPermittedDocumentIdsArbitraryPermission:
|
class TestPermittedDocumentIdsArbitraryPermission:
|
||||||
def test_change_document_permission_is_distinct_from_view(self):
|
def test_change_document_permission_is_distinct_from_view(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
viewer_only = User.objects.create_user(username="viewer")
|
viewer_only = UserFactory(username="viewer")
|
||||||
editor = User.objects.create_user(username="editor")
|
editor = UserFactory(username="editor")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", viewer_only, doc)
|
grant_object(viewer_only, doc, "view_document")
|
||||||
assign_perm("change_document", editor, doc)
|
grant_object(editor, doc, "change_document")
|
||||||
assign_perm("view_document", editor, doc)
|
grant_object(editor, doc, "view_document")
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_document_ids(editor, perm="change_document"),
|
permitted_document_ids(editor, perm="change_document"),
|
||||||
@@ -257,10 +259,10 @@ class TestPermittedDocumentIdsArbitraryPermission:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_qualified_permission_string_is_normalized_to_codename(self):
|
def test_qualified_permission_string_is_normalized_to_codename(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
editor = User.objects.create_user(username="editor")
|
editor = UserFactory(username="editor")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
assign_perm("change_document", editor, doc)
|
grant_object(editor, doc, "change_document")
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_document_ids(editor, perm="documents.change_document"),
|
permitted_document_ids(editor, perm="documents.change_document"),
|
||||||
@@ -269,11 +271,11 @@ class TestPermittedDocumentIdsArbitraryPermission:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_permission_with_include_deleted_for_trash_restore(self):
|
def test_delete_permission_with_include_deleted_for_trash_restore(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
stranger = User.objects.create_user(username="mallory")
|
stranger = UserFactory(username="mallory")
|
||||||
view_only = User.objects.create_user(username="viewer")
|
view_only = UserFactory(username="viewer")
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", view_only, doc)
|
grant_object(view_only, doc, "view_document")
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
@@ -307,11 +309,9 @@ class TestEmailDocumentPermissionBoundary:
|
|||||||
self,
|
self,
|
||||||
rest_api_client,
|
rest_api_client,
|
||||||
):
|
):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
hidden = DocumentFactory(owner=owner)
|
hidden = DocumentFactory(owner=owner)
|
||||||
|
|
||||||
@@ -339,19 +339,17 @@ class TestBulkEditChangePermissionBoundary:
|
|||||||
# permitted document must not be partially applied just because it
|
# permitted document must not be partially applied just because it
|
||||||
# was bundled with a forbidden one, proving the endpoint checks
|
# was bundled with a forbidden one, proving the endpoint checks
|
||||||
# every document in the batch rather than only the first/last.
|
# every document in the batch rather than only the first/last.
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
# grant the global change_document permission so the object-level
|
# grant the global change_document permission so the object-level
|
||||||
# check (not the global has_perm check) is what's under test
|
# check (not the global has_perm check) is what's under test
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "change_document")
|
||||||
Permission.objects.get(codename="change_document"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
changeable = DocumentFactory(owner=owner)
|
changeable = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", requester, changeable)
|
grant_object(requester, changeable, "view_document")
|
||||||
assign_perm("change_document", requester, changeable) # fully permitted
|
grant_object(requester, changeable, "change_document") # fully permitted
|
||||||
target = DocumentFactory(owner=owner)
|
target = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", requester, target) # view only, NOT change
|
grant_object(requester, target, "view_document") # view only, NOT change
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
"/api/documents/bulk_edit/",
|
"/api/documents/bulk_edit/",
|
||||||
@@ -369,15 +367,14 @@ class TestBulkEditChangePermissionBoundary:
|
|||||||
class TestBulkDownloadPermissionChecksRootDocument:
|
class TestBulkDownloadPermissionChecksRootDocument:
|
||||||
def test_download_requires_global_view_permission(
|
def test_download_requires_global_view_permission(
|
||||||
self,
|
self,
|
||||||
rest_api_client,
|
rest_api_client: APIClient,
|
||||||
paperless_dirs,
|
paperless_dirs: PaperlessDirs,
|
||||||
_media_settings,
|
) -> None:
|
||||||
):
|
owner = UserFactory(username="owner")
|
||||||
owner = User.objects.create_user(username="owner")
|
requester = UserFactory(username="requester")
|
||||||
requester = User.objects.create_user(username="requester")
|
|
||||||
root = DocumentFactory(owner=owner)
|
root = DocumentFactory(owner=owner)
|
||||||
root.source_path.write_bytes(b"%PDF-1.4 test")
|
root.source_path.write_bytes(b"%PDF-1.4 test")
|
||||||
assign_perm("view_document", requester, root)
|
grant_object(requester, root, "view_document")
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
@@ -390,21 +387,18 @@ class TestBulkDownloadPermissionChecksRootDocument:
|
|||||||
|
|
||||||
def test_permission_checked_on_root_not_on_version(
|
def test_permission_checked_on_root_not_on_version(
|
||||||
self,
|
self,
|
||||||
rest_api_client,
|
rest_api_client: APIClient,
|
||||||
paperless_dirs,
|
paperless_dirs: PaperlessDirs,
|
||||||
_media_settings,
|
) -> None:
|
||||||
):
|
owner = UserFactory(username="owner")
|
||||||
owner = User.objects.create_user(username="owner")
|
requester = UserFactory(username="requester")
|
||||||
requester = User.objects.create_user(username="requester")
|
grant_global(requester, "view_document")
|
||||||
requester.user_permissions.add(
|
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
root = DocumentFactory(owner=owner)
|
root = DocumentFactory(owner=owner)
|
||||||
# a version of root that the requester has NOT been individually granted
|
# a version of root that the requester has NOT been individually granted
|
||||||
version = DocumentFactory(owner=owner, root_document=root, version_index=1)
|
version = DocumentFactory(owner=owner, root_document=root, version_index=1)
|
||||||
version.source_path.write_bytes(b"%PDF-1.4 test")
|
version.source_path.write_bytes(b"%PDF-1.4 test")
|
||||||
assign_perm("view_document", requester, root) # granted on ROOT only
|
grant_object(requester, root, "view_document") # granted on ROOT only
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
"/api/documents/bulk_download/",
|
"/api/documents/bulk_download/",
|
||||||
@@ -422,11 +416,9 @@ class TestBulkDownloadPermissionChecksRootDocument:
|
|||||||
# root-or-version bug; a user with no grant at all (the old
|
# root-or-version bug; a user with no grant at all (the old
|
||||||
# `stranger` case) can't tell the two apart, since they're denied
|
# `stranger` case) can't tell the two apart, since they're denied
|
||||||
# either way.
|
# either way.
|
||||||
version_only_grantee = User.objects.create_user(username="version_only_grantee")
|
version_only_grantee = UserFactory(username="version_only_grantee")
|
||||||
version_only_grantee.user_permissions.add(
|
grant_global(version_only_grantee, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
grant_object(version_only_grantee, version, "view_document")
|
||||||
)
|
|
||||||
assign_perm("view_document", version_only_grantee, version)
|
|
||||||
rest_api_client.force_authenticate(user=version_only_grantee)
|
rest_api_client.force_authenticate(user=version_only_grantee)
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
"/api/documents/bulk_download/",
|
"/api/documents/bulk_download/",
|
||||||
@@ -445,14 +437,12 @@ class TestTrashRestorePermissionBoundary:
|
|||||||
self,
|
self,
|
||||||
rest_api_client,
|
rest_api_client,
|
||||||
):
|
):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "delete_document")
|
||||||
Permission.objects.get(codename="delete_document"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
assign_perm("view_document", requester, doc) # view only, NOT delete
|
grant_object(requester, doc, "view_document") # view only, NOT delete
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
@@ -466,14 +456,12 @@ class TestTrashRestorePermissionBoundary:
|
|||||||
self,
|
self,
|
||||||
rest_api_client,
|
rest_api_client,
|
||||||
):
|
):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "delete_document")
|
||||||
Permission.objects.get(codename="delete_document"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
assign_perm("delete_document", requester, doc)
|
grant_object(requester, doc, "delete_document")
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
@@ -484,11 +472,11 @@ class TestTrashRestorePermissionBoundary:
|
|||||||
assert response.status_code == HTTPStatus.OK
|
assert response.status_code == HTTPStatus.OK
|
||||||
|
|
||||||
def test_restore_requires_global_delete_permission(self, rest_api_client):
|
def test_restore_requires_global_delete_permission(self, rest_api_client):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
assign_perm("delete_document", requester, doc)
|
grant_object(requester, doc, "delete_document")
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
@@ -513,14 +501,12 @@ class TestTrashViewExcludesExplicitlyGrantedDocuments:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def test_explicit_grant_does_not_leak_trashed_document(self, rest_api_client):
|
def test_explicit_grant_does_not_leak_trashed_document(self, rest_api_client):
|
||||||
owner = User.objects.create_user(username="trash_owner")
|
owner = UserFactory(username="trash_owner")
|
||||||
grantee = User.objects.create_user(username="trash_grantee")
|
grantee = UserFactory(username="trash_grantee")
|
||||||
grantee.user_permissions.add(
|
grant_global(grantee, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
doc = DocumentFactory(owner=owner)
|
doc = DocumentFactory(owner=owner)
|
||||||
doc.delete() # soft delete
|
doc.delete() # soft delete
|
||||||
assign_perm("view_document", grantee, doc)
|
grant_object(grantee, doc, "view_document")
|
||||||
|
|
||||||
rest_api_client.force_authenticate(user=grantee)
|
rest_api_client.force_authenticate(user=grantee)
|
||||||
response = rest_api_client.get("/api/trash/")
|
response = rest_api_client.get("/api/trash/")
|
||||||
@@ -542,8 +528,8 @@ class TestTrashViewExcludesExplicitlyGrantedDocuments:
|
|||||||
)
|
)
|
||||||
class TestPermittedObjectIdsGenericModels:
|
class TestPermittedObjectIdsGenericModels:
|
||||||
def test_owner_sees_own_object(self, model, factory, perm):
|
def test_owner_sees_own_object(self, model, factory, perm):
|
||||||
owner = User.objects.create_user(username=f"owner_{model.__name__}")
|
owner = UserFactory(username=f"owner_{model.__name__}")
|
||||||
stranger = User.objects.create_user(username=f"stranger_{model.__name__}")
|
stranger = UserFactory(username=f"stranger_{model.__name__}")
|
||||||
owned = factory(owner=owner)
|
owned = factory(owner=owner)
|
||||||
strangers = factory(owner=stranger)
|
strangers = factory(owner=stranger)
|
||||||
|
|
||||||
@@ -556,14 +542,14 @@ class TestPermittedObjectIdsGenericModels:
|
|||||||
@pytest.mark.parametrize("is_superuser", [False, True])
|
@pytest.mark.parametrize("is_superuser", [False, True])
|
||||||
def test_inactive_user_sees_nothing(self, model, factory, perm, is_superuser):
|
def test_inactive_user_sees_nothing(self, model, factory, perm, is_superuser):
|
||||||
suffix = f"{model.__name__}_{is_superuser}"
|
suffix = f"{model.__name__}_{is_superuser}"
|
||||||
user = User.objects.create_user(
|
user = UserFactory(
|
||||||
username=f"inactive_{suffix}",
|
username=f"inactive_{suffix}",
|
||||||
is_active=False,
|
is_active=False,
|
||||||
is_superuser=is_superuser,
|
is_superuser=is_superuser,
|
||||||
)
|
)
|
||||||
other = User.objects.create_user(username=f"other_{suffix}")
|
other = UserFactory(username=f"other_{suffix}")
|
||||||
granted = factory(owner=other)
|
granted = factory(owner=other)
|
||||||
assign_perm(perm, user, granted)
|
grant_object(user, granted, perm)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_object_ids(user, model, perm),
|
permitted_object_ids(user, model, perm),
|
||||||
@@ -576,7 +562,7 @@ class TestPermittedObjectIdsGenericModels:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_unowned_object_visible_to_everyone(self, model, factory, perm):
|
def test_unowned_object_visible_to_everyone(self, model, factory, perm):
|
||||||
user = User.objects.create_user(username=f"user_{model.__name__}")
|
user = UserFactory(username=f"user_{model.__name__}")
|
||||||
unowned = factory(owner=None)
|
unowned = factory(owner=None)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
@@ -586,12 +572,12 @@ class TestPermittedObjectIdsGenericModels:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_explicit_permission_grants_visibility(self, model, factory, perm):
|
def test_explicit_permission_grants_visibility(self, model, factory, perm):
|
||||||
owner = User.objects.create_user(username=f"owner2_{model.__name__}")
|
owner = UserFactory(username=f"owner2_{model.__name__}")
|
||||||
grantee = User.objects.create_user(username=f"grantee_{model.__name__}")
|
grantee = UserFactory(username=f"grantee_{model.__name__}")
|
||||||
stranger = User.objects.create_user(username=f"stranger2_{model.__name__}")
|
stranger = UserFactory(username=f"stranger2_{model.__name__}")
|
||||||
shared = factory(owner=owner)
|
shared = factory(owner=owner)
|
||||||
not_shared = factory(owner=owner)
|
not_shared = factory(owner=owner)
|
||||||
assign_perm(perm, grantee, shared)
|
grant_object(grantee, shared, perm)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_object_ids(grantee, model, perm),
|
permitted_object_ids(grantee, model, perm),
|
||||||
@@ -610,13 +596,13 @@ class TestPermittedObjectIdsGenericModels:
|
|||||||
factory,
|
factory,
|
||||||
perm,
|
perm,
|
||||||
):
|
):
|
||||||
owner = User.objects.create_user(username=f"owner3_{model.__name__}")
|
owner = UserFactory(username=f"owner3_{model.__name__}")
|
||||||
member = User.objects.create_user(username=f"member_{model.__name__}")
|
member = UserFactory(username=f"member_{model.__name__}")
|
||||||
non_member = User.objects.create_user(username=f"nonmember_{model.__name__}")
|
non_member = UserFactory(username=f"nonmember_{model.__name__}")
|
||||||
group = Group.objects.create(name=f"group_{model.__name__}")
|
group = Group.objects.create(name=f"group_{model.__name__}")
|
||||||
member.groups.add(group)
|
member.groups.add(group)
|
||||||
shared = factory(owner=owner)
|
shared = factory(owner=owner)
|
||||||
assign_perm(perm, group, shared)
|
grant_object(group, shared, perm)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
permitted_object_ids(member, model, perm),
|
permitted_object_ids(member, model, perm),
|
||||||
@@ -630,8 +616,8 @@ class TestPermittedObjectIdsGenericModels:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_superuser_sees_everything(self, model, factory, perm):
|
def test_superuser_sees_everything(self, model, factory, perm):
|
||||||
superuser = User.objects.create_superuser(username=f"root_{model.__name__}")
|
superuser = UserFactory(username=f"root_{model.__name__}", superuser=True)
|
||||||
owner = User.objects.create_user(username=f"owner4_{model.__name__}")
|
owner = UserFactory(username=f"owner4_{model.__name__}")
|
||||||
obj = factory(owner=owner)
|
obj = factory(owner=owner)
|
||||||
|
|
||||||
assert_visible_document_ids(
|
assert_visible_document_ids(
|
||||||
@@ -644,8 +630,8 @@ class TestPermittedObjectIdsGenericModels:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestMatchingRespectsObjectPermissions:
|
class TestMatchingRespectsObjectPermissions:
|
||||||
def test_match_tags_only_considers_tags_visible_to_user(self):
|
def test_match_tags_only_considers_tags_visible_to_user(self):
|
||||||
owner = User.objects.create_user(username="tag_owner")
|
owner = UserFactory(username="tag_owner")
|
||||||
classifying_user = User.objects.create_user(username="classifier_user")
|
classifying_user = UserFactory(username="classifier_user")
|
||||||
visible_tag = TagFactory(
|
visible_tag = TagFactory(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
match="invoice",
|
match="invoice",
|
||||||
@@ -656,7 +642,7 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
match="invoice",
|
match="invoice",
|
||||||
matching_algorithm=Tag.MATCH_LITERAL,
|
matching_algorithm=Tag.MATCH_LITERAL,
|
||||||
)
|
)
|
||||||
assign_perm("view_tag", classifying_user, visible_tag)
|
grant_object(classifying_user, visible_tag, "view_tag")
|
||||||
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
||||||
|
|
||||||
matched = match_tags(doc, classifier=None, user=classifying_user)
|
matched = match_tags(doc, classifier=None, user=classifying_user)
|
||||||
@@ -665,8 +651,8 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
assert hidden_tag.pk not in matched_ids
|
assert hidden_tag.pk not in matched_ids
|
||||||
|
|
||||||
def test_match_correspondents_only_considers_correspondents_visible_to_user(self):
|
def test_match_correspondents_only_considers_correspondents_visible_to_user(self):
|
||||||
owner = User.objects.create_user(username="correspondent_owner")
|
owner = UserFactory(username="correspondent_owner")
|
||||||
classifying_user = User.objects.create_user(username="classifier_user2")
|
classifying_user = UserFactory(username="classifier_user2")
|
||||||
visible_correspondent = CorrespondentFactory(
|
visible_correspondent = CorrespondentFactory(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
match="invoice",
|
match="invoice",
|
||||||
@@ -677,7 +663,7 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
match="invoice",
|
match="invoice",
|
||||||
matching_algorithm=Correspondent.MATCH_LITERAL,
|
matching_algorithm=Correspondent.MATCH_LITERAL,
|
||||||
)
|
)
|
||||||
assign_perm("view_correspondent", classifying_user, visible_correspondent)
|
grant_object(classifying_user, visible_correspondent, "view_correspondent")
|
||||||
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
||||||
|
|
||||||
matched = match_correspondents(doc, classifier=None, user=classifying_user)
|
matched = match_correspondents(doc, classifier=None, user=classifying_user)
|
||||||
@@ -686,8 +672,8 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
assert hidden_correspondent.pk not in matched_ids
|
assert hidden_correspondent.pk not in matched_ids
|
||||||
|
|
||||||
def test_match_document_types_only_considers_document_types_visible_to_user(self):
|
def test_match_document_types_only_considers_document_types_visible_to_user(self):
|
||||||
owner = User.objects.create_user(username="document_type_owner")
|
owner = UserFactory(username="document_type_owner")
|
||||||
classifying_user = User.objects.create_user(username="classifier_user3")
|
classifying_user = UserFactory(username="classifier_user3")
|
||||||
visible_document_type = DocumentTypeFactory(
|
visible_document_type = DocumentTypeFactory(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
match="invoice",
|
match="invoice",
|
||||||
@@ -698,7 +684,7 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
match="invoice",
|
match="invoice",
|
||||||
matching_algorithm=DocumentType.MATCH_LITERAL,
|
matching_algorithm=DocumentType.MATCH_LITERAL,
|
||||||
)
|
)
|
||||||
assign_perm("view_documenttype", classifying_user, visible_document_type)
|
grant_object(classifying_user, visible_document_type, "view_documenttype")
|
||||||
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
||||||
|
|
||||||
matched = match_document_types(doc, classifier=None, user=classifying_user)
|
matched = match_document_types(doc, classifier=None, user=classifying_user)
|
||||||
@@ -707,8 +693,8 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
assert hidden_document_type.pk not in matched_ids
|
assert hidden_document_type.pk not in matched_ids
|
||||||
|
|
||||||
def test_match_storage_paths_only_considers_storage_paths_visible_to_user(self):
|
def test_match_storage_paths_only_considers_storage_paths_visible_to_user(self):
|
||||||
owner = User.objects.create_user(username="storage_path_owner")
|
owner = UserFactory(username="storage_path_owner")
|
||||||
classifying_user = User.objects.create_user(username="classifier_user4")
|
classifying_user = UserFactory(username="classifier_user4")
|
||||||
visible_storage_path = StoragePathFactory(
|
visible_storage_path = StoragePathFactory(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
match="invoice",
|
match="invoice",
|
||||||
@@ -719,7 +705,7 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
match="invoice",
|
match="invoice",
|
||||||
matching_algorithm=StoragePath.MATCH_LITERAL,
|
matching_algorithm=StoragePath.MATCH_LITERAL,
|
||||||
)
|
)
|
||||||
assign_perm("view_storagepath", classifying_user, visible_storage_path)
|
grant_object(classifying_user, visible_storage_path, "view_storagepath")
|
||||||
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
|
||||||
|
|
||||||
matched = match_storage_paths(doc, classifier=None, user=classifying_user)
|
matched = match_storage_paths(doc, classifier=None, user=classifying_user)
|
||||||
@@ -731,14 +717,12 @@ class TestMatchingRespectsObjectPermissions:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestBulkEditObjectsApplyToAllPermissionBoundary:
|
class TestBulkEditObjectsApplyToAllPermissionBoundary:
|
||||||
def test_apply_to_all_tags_excludes_unpermitted_tag(self, rest_api_client):
|
def test_apply_to_all_tags_excludes_unpermitted_tag(self, rest_api_client):
|
||||||
owner = User.objects.create_user(username="tags_owner")
|
owner = UserFactory(username="tags_owner")
|
||||||
requester = User.objects.create_user(username="tags_requester")
|
requester = UserFactory(username="tags_requester")
|
||||||
new_owner = User.objects.create_user(username="tags_new_owner")
|
new_owner = UserFactory(username="tags_new_owner")
|
||||||
# grant the global change_tag permission so the object-level
|
# grant the global change_tag permission so the object-level
|
||||||
# filtering (not the global has_perm check) is what's under test
|
# filtering (not the global has_perm check) is what's under test
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "change_tag")
|
||||||
Permission.objects.get(codename="change_tag"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
visible = TagFactory(owner=requester)
|
visible = TagFactory(owner=requester)
|
||||||
hidden = TagFactory(owner=owner)
|
hidden = TagFactory(owner=owner)
|
||||||
@@ -771,16 +755,14 @@ class TestBulkEditObjectsApplyToAllPermissionBoundary:
|
|||||||
request rather than being silently skipped. Editing permissions is
|
request rather than being silently skipped. Editing permissions is
|
||||||
limited to the owner, same as documents.
|
limited to the owner, same as documents.
|
||||||
"""
|
"""
|
||||||
owner = User.objects.create_user(username="shared_tags_owner")
|
owner = UserFactory(username="shared_tags_owner")
|
||||||
requester = User.objects.create_user(username="shared_tags_requester")
|
requester = UserFactory(username="shared_tags_requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "change_tag")
|
||||||
Permission.objects.get(codename="change_tag"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
owned = TagFactory(owner=requester)
|
owned = TagFactory(owner=requester)
|
||||||
shared = TagFactory(owner=owner)
|
shared = TagFactory(owner=owner)
|
||||||
assign_perm("view_tag", requester, shared)
|
grant_object(requester, shared, "view_tag")
|
||||||
assign_perm("change_tag", requester, shared)
|
grant_object(requester, shared, "change_tag")
|
||||||
|
|
||||||
response = rest_api_client.post(
|
response = rest_api_client.post(
|
||||||
"/api/bulk_edit_objects/",
|
"/api/bulk_edit_objects/",
|
||||||
@@ -831,14 +813,12 @@ class TestBulkEditObjectsTagDescendantPartialPermission:
|
|||||||
would pass/fail based on FK cascade behavior, not on whether the
|
would pass/fail based on FK cascade behavior, not on whether the
|
||||||
descendant-expansion logic itself respected per-object permissions.
|
descendant-expansion logic itself respected per-object permissions.
|
||||||
"""
|
"""
|
||||||
owner = User.objects.create_user(username="tag_hierarchy_owner")
|
owner = UserFactory(username="tag_hierarchy_owner")
|
||||||
requester = User.objects.create_user(username="tag_hierarchy_requester")
|
requester = UserFactory(username="tag_hierarchy_requester")
|
||||||
new_owner = User.objects.create_user(username="tag_hierarchy_new_owner")
|
new_owner = UserFactory(username="tag_hierarchy_new_owner")
|
||||||
# global change_tag permission so the has_perm() gate passes and the
|
# global change_tag permission so the has_perm() gate passes and the
|
||||||
# object-level permitted_object_ids filtering is what's under test
|
# object-level permitted_object_ids filtering is what's under test
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "change_tag")
|
||||||
Permission.objects.get(codename="change_tag"),
|
|
||||||
)
|
|
||||||
rest_api_client.force_authenticate(user=requester)
|
rest_api_client.force_authenticate(user=requester)
|
||||||
|
|
||||||
parent = TagFactory(owner=requester, name="parent-tag")
|
parent = TagFactory(owner=requester, name="parent-tag")
|
||||||
@@ -890,7 +870,7 @@ class TestRestrictQuerysetToVisible:
|
|||||||
- The queryset is returned unfiltered, rather than
|
- The queryset is returned unfiltered, rather than
|
||||||
permitted_object_ids(None, ...)'s narrower "unowned rows only"
|
permitted_object_ids(None, ...)'s narrower "unowned rows only"
|
||||||
"""
|
"""
|
||||||
owner = User.objects.create_user(username="vis_none_owner")
|
owner = UserFactory(username="vis_none_owner")
|
||||||
tag = TagFactory(owner=owner)
|
tag = TagFactory(owner=owner)
|
||||||
|
|
||||||
visible = restrict_queryset_to_visible(Tag.objects.all(), None, "view_tag")
|
visible = restrict_queryset_to_visible(Tag.objects.all(), None, "view_tag")
|
||||||
@@ -907,8 +887,8 @@ class TestRestrictQuerysetToVisible:
|
|||||||
- The queryset is returned unfiltered, skipping the permission
|
- The queryset is returned unfiltered, skipping the permission
|
||||||
lookup entirely
|
lookup entirely
|
||||||
"""
|
"""
|
||||||
superuser = User.objects.create_superuser(username="vis_active_super")
|
superuser = UserFactory(username="vis_active_super", superuser=True)
|
||||||
owner = User.objects.create_user(username="vis_active_super_owner")
|
owner = UserFactory(username="vis_active_super_owner")
|
||||||
tag = TagFactory(owner=owner)
|
tag = TagFactory(owner=owner)
|
||||||
|
|
||||||
visible = restrict_queryset_to_visible(
|
visible = restrict_queryset_to_visible(
|
||||||
@@ -930,7 +910,7 @@ class TestRestrictQuerysetToVisible:
|
|||||||
deactivation has to win over the superuser shortcut, matching
|
deactivation has to win over the superuser shortcut, matching
|
||||||
permitted_object_ids's own ordering
|
permitted_object_ids's own ordering
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(
|
||||||
username="vis_inactive_super",
|
username="vis_inactive_super",
|
||||||
is_active=False,
|
is_active=False,
|
||||||
is_superuser=True,
|
is_superuser=True,
|
||||||
@@ -951,8 +931,8 @@ class TestRestrictQuerysetToVisible:
|
|||||||
THEN:
|
THEN:
|
||||||
- Only the rows permitted_object_ids() reports are visible
|
- Only the rows permitted_object_ids() reports are visible
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(username="vis_regular")
|
user = UserFactory(username="vis_regular")
|
||||||
other = User.objects.create_user(username="vis_regular_other")
|
other = UserFactory(username="vis_regular_other")
|
||||||
own = TagFactory(owner=user)
|
own = TagFactory(owner=user)
|
||||||
hidden = TagFactory(owner=other)
|
hidden = TagFactory(owner=other)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
from documents.filters import PermittedObjectsFilter
|
from documents.filters import PermittedObjectsFilter
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.factories import TagFactory
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class _DummyView:
|
class _DummyView:
|
||||||
@@ -15,8 +15,8 @@ class _DummyView:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestPermittedObjectsFilter:
|
class TestPermittedObjectsFilter:
|
||||||
def test_superuser_bypasses_filtering_entirely(self):
|
def test_superuser_bypasses_filtering_entirely(self):
|
||||||
superuser = User.objects.create_superuser(username="root")
|
superuser = UserFactory(username="root", superuser=True)
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
TagFactory(owner=owner)
|
TagFactory(owner=owner)
|
||||||
request = APIRequestFactory().get("/")
|
request = APIRequestFactory().get("/")
|
||||||
request.user = superuser
|
request.user = superuser
|
||||||
@@ -29,13 +29,13 @@ class TestPermittedObjectsFilter:
|
|||||||
assert result.count() == Tag.objects.count()
|
assert result.count() == Tag.objects.count()
|
||||||
|
|
||||||
def test_non_superuser_sees_only_owned_unowned_and_granted(self):
|
def test_non_superuser_sees_only_owned_unowned_and_granted(self):
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
grantee = User.objects.create_user(username="grantee")
|
grantee = UserFactory(username="grantee")
|
||||||
owned = TagFactory(owner=grantee)
|
owned = TagFactory(owner=grantee)
|
||||||
unowned = TagFactory(owner=None)
|
unowned = TagFactory(owner=None)
|
||||||
granted = TagFactory(owner=owner)
|
granted = TagFactory(owner=owner)
|
||||||
hidden = TagFactory(owner=owner)
|
hidden = TagFactory(owner=owner)
|
||||||
assign_perm("view_tag", grantee, granted)
|
grant_object(grantee, granted, "view_tag")
|
||||||
request = APIRequestFactory().get("/")
|
request = APIRequestFactory().get("/")
|
||||||
request.user = grantee
|
request.user = grantee
|
||||||
|
|
||||||
@@ -49,11 +49,11 @@ class TestPermittedObjectsFilter:
|
|||||||
assert hidden.pk not in visible_ids
|
assert hidden.pk not in visible_ids
|
||||||
|
|
||||||
def test_include_granted_false_excludes_explicitly_shared_objects(self):
|
def test_include_granted_false_excludes_explicitly_shared_objects(self):
|
||||||
owner = User.objects.create_user(username="owner2")
|
owner = UserFactory(username="owner2")
|
||||||
grantee = User.objects.create_user(username="grantee2")
|
grantee = UserFactory(username="grantee2")
|
||||||
owned = TagFactory(owner=grantee)
|
owned = TagFactory(owner=grantee)
|
||||||
granted = TagFactory(owner=owner)
|
granted = TagFactory(owner=owner)
|
||||||
assign_perm("view_tag", grantee, granted)
|
grant_object(grantee, granted, "view_tag")
|
||||||
request = APIRequestFactory().get("/")
|
request = APIRequestFactory().get("/")
|
||||||
request.user = grantee
|
request.user = grantee
|
||||||
|
|
||||||
@@ -74,15 +74,15 @@ class TestPermittedObjectsFilter:
|
|||||||
[("inactive", False), ("inactive_super", True)],
|
[("inactive", False), ("inactive_super", True)],
|
||||||
)
|
)
|
||||||
def test_inactive_user_sees_nothing(self, username: str, *, is_superuser: bool):
|
def test_inactive_user_sees_nothing(self, username: str, *, is_superuser: bool):
|
||||||
user = User.objects.create_user(
|
user = UserFactory(
|
||||||
username=username,
|
username=username,
|
||||||
is_active=False,
|
is_active=False,
|
||||||
is_superuser=is_superuser,
|
is_superuser=is_superuser,
|
||||||
)
|
)
|
||||||
TagFactory(owner=None)
|
TagFactory(owner=None)
|
||||||
TagFactory(owner=user)
|
TagFactory(owner=user)
|
||||||
granted = TagFactory(owner=User.objects.create_user(username=f"o_{username}"))
|
granted = TagFactory(owner=UserFactory(username=f"o_{username}"))
|
||||||
assign_perm("view_tag", user, granted)
|
grant_object(user, granted, "view_tag")
|
||||||
request = APIRequestFactory().get("/")
|
request = APIRequestFactory().get("/")
|
||||||
request.user = user
|
request.user = user
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class TestPermittedObjectsFilter:
|
|||||||
assert result.count() == 0
|
assert result.count() == 0
|
||||||
|
|
||||||
def test_inactive_user_sees_nothing_with_include_granted_false(self):
|
def test_inactive_user_sees_nothing_with_include_granted_false(self):
|
||||||
user = User.objects.create_user(username="inactive_owner", is_active=False)
|
user = UserFactory(username="inactive_owner", is_active=False)
|
||||||
TagFactory(owner=user)
|
TagFactory(owner=user)
|
||||||
TagFactory(owner=None)
|
TagFactory(owner=None)
|
||||||
request = APIRequestFactory().get("/")
|
request = APIRequestFactory().get("/")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
|||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.conftest import PaperlessDirs
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
|
|
||||||
class TestSanityCheckMessages:
|
class TestSanityCheckMessages:
|
||||||
@@ -46,14 +46,14 @@ class TestSanityCheckMessages:
|
|||||||
class TestCheckSanityNoDocuments:
|
class TestCheckSanityNoDocuments:
|
||||||
"""Sanity checks against an empty archive."""
|
"""Sanity checks against an empty archive."""
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
@pytest.mark.usefixtures("paperless_dirs")
|
||||||
def test_no_documents(self) -> None:
|
def test_no_documents(self) -> None:
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
assert not messages.has_error
|
assert not messages.has_error
|
||||||
assert not messages.has_warning
|
assert not messages.has_warning
|
||||||
assert messages.total_issue_count == 0
|
assert messages.total_issue_count == 0
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
@pytest.mark.usefixtures("paperless_dirs")
|
||||||
def test_no_issues_logs_clean(self, caplog: pytest.LogCaptureFixture) -> None:
|
def test_no_issues_logs_clean(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
with caplog.at_level(logging.INFO, logger="paperless.sanity_checker"):
|
with caplog.at_level(logging.INFO, logger="paperless.sanity_checker"):
|
||||||
@@ -214,18 +214,17 @@ class TestCheckSanityOrphans:
|
|||||||
sample_doc: Document,
|
sample_doc: Document,
|
||||||
paperless_dirs: PaperlessDirs,
|
paperless_dirs: PaperlessDirs,
|
||||||
) -> None:
|
) -> None:
|
||||||
(paperless_dirs.originals / "orphan.pdf").touch()
|
(paperless_dirs.originals_dir / "orphan.pdf").touch()
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
assert messages.has_warning
|
assert messages.has_warning
|
||||||
assert any("Orphaned file" in m["message"] for m in messages[None])
|
assert any("Orphaned file" in m["message"] for m in messages[None])
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
|
||||||
def test_ignorable_files_not_flagged(
|
def test_ignorable_files_not_flagged(
|
||||||
self,
|
self,
|
||||||
paperless_dirs: PaperlessDirs,
|
paperless_dirs: PaperlessDirs,
|
||||||
) -> None:
|
) -> None:
|
||||||
(paperless_dirs.media / ".DS_Store").touch()
|
(paperless_dirs.media_dir / ".DS_Store").touch()
|
||||||
(paperless_dirs.media / "desktop.ini").touch()
|
(paperless_dirs.media_dir / "desktop.ini").touch()
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
assert not messages.has_warning
|
assert not messages.has_warning
|
||||||
|
|
||||||
@@ -269,13 +268,13 @@ class TestCheckSanityLogMessages:
|
|||||||
paperless_dirs: PaperlessDirs,
|
paperless_dirs: PaperlessDirs,
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
(paperless_dirs.originals / "orphan.pdf").touch()
|
(paperless_dirs.originals_dir / "orphan.pdf").touch()
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
with caplog.at_level(logging.WARNING, logger="paperless.sanity_checker"):
|
with caplog.at_level(logging.WARNING, logger="paperless.sanity_checker"):
|
||||||
messages.log_messages()
|
messages.log_messages()
|
||||||
assert "Orphaned file" in caplog.text
|
assert "Orphaned file" in caplog.text
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_media_settings")
|
@pytest.mark.usefixtures("paperless_dirs")
|
||||||
def test_logs_unknown_doc_pk(self, caplog: pytest.LogCaptureFixture) -> None:
|
def test_logs_unknown_doc_pk(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
"""A doc PK not in the DB logs 'Unknown' as the title."""
|
"""A doc PK not in the DB logs 'Unknown' as the title."""
|
||||||
messages = check_sanity()
|
messages = check_sanity()
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ from pathlib import Path
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
@@ -20,8 +18,11 @@ from documents.models import ShareLinkBundle
|
|||||||
from documents.serialisers import ShareLinkBundleSerializer
|
from documents.serialisers import ShareLinkBundleSerializer
|
||||||
from documents.tasks import build_share_link_bundle
|
from documents.tasks import build_share_link_bundle
|
||||||
from documents.tasks import cleanup_expired_share_link_bundles
|
from documents.tasks import cleanup_expired_share_link_bundles
|
||||||
from documents.tests.factories import DocumentFactory
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
||||||
@@ -29,7 +30,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create_superuser(username="bundle_admin")
|
self.user = UserFactory(username="bundle_admin", superuser=True)
|
||||||
self.client.force_authenticate(self.user)
|
self.client.force_authenticate(self.user)
|
||||||
self.document = DocumentFactory.create()
|
self.document = DocumentFactory.create()
|
||||||
|
|
||||||
@@ -55,13 +56,11 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
self,
|
self,
|
||||||
delay_mock,
|
delay_mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
owner = User.objects.create_user(username="document_owner")
|
owner = UserFactory(username="document_owner")
|
||||||
requester = User.objects.create_user(username="bundle_creator")
|
requester = UserFactory(username="bundle_creator")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "add_sharelinkbundle")
|
||||||
Permission.objects.get(codename="add_sharelinkbundle"),
|
|
||||||
)
|
|
||||||
document = DocumentFactory.create(owner=owner)
|
document = DocumentFactory.create(owner=owner)
|
||||||
assign_perm("view_document", requester, document)
|
grant_object(requester, document, "view_document")
|
||||||
self.client.force_authenticate(requester)
|
self.client.force_authenticate(requester)
|
||||||
payload = {
|
payload = {
|
||||||
"document_ids": [document.pk],
|
"document_ids": [document.pk],
|
||||||
@@ -72,9 +71,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
response = self.client.post(self.ENDPOINT, payload, format="json")
|
response = self.client.post(self.ENDPOINT, payload, format="json")
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
requester = User.objects.get(pk=requester.pk)
|
requester = User.objects.get(pk=requester.pk)
|
||||||
self.client.force_authenticate(requester)
|
self.client.force_authenticate(requester)
|
||||||
response = self.client.post(self.ENDPOINT, payload, format="json")
|
response = self.client.post(self.ENDPOINT, payload, format="json")
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@@ -13,16 +11,16 @@ from documents.models import WorkflowAction
|
|||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.serialisers import TagSerializer
|
from documents.serialisers import TagSerializer
|
||||||
from documents.signals.handlers import run_workflows
|
from documents.signals.handlers import run_workflows
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
|
||||||
|
|
||||||
class TestTagHierarchyPermissions(APITestCase):
|
class TestTagHierarchyPermissions(APITestCase):
|
||||||
def test_children_only_include_visible_tags(self) -> None:
|
def test_children_only_include_visible_tags(self) -> None:
|
||||||
owner = User.objects.create_user(username="owner")
|
owner = UserFactory(username="owner")
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "view_tag")
|
||||||
Permission.objects.get(codename="view_tag"),
|
|
||||||
)
|
|
||||||
parent = Tag.objects.create(name="Visible parent", owner=requester)
|
parent = Tag.objects.create(name="Visible parent", owner=requester)
|
||||||
hidden_child = Tag.objects.create(
|
hidden_child = Tag.objects.create(
|
||||||
name="Hidden child",
|
name="Hidden child",
|
||||||
@@ -49,7 +47,7 @@ class TestTagHierarchyPermissions(APITestCase):
|
|||||||
class TestTagHierarchy(DirectoriesMixin, APITestCase):
|
class TestTagHierarchy(DirectoriesMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create_superuser(username="admin")
|
self.user = UserFactory(username="admin", superuser=True)
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
self.parent = Tag.objects.create(name="Parent")
|
self.parent = Tag.objects.create(name="Parent")
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ from documents.signals.handlers import task_failure_handler
|
|||||||
from documents.signals.handlers import task_postrun_handler
|
from documents.signals.handlers import task_postrun_handler
|
||||||
from documents.signals.handlers import task_prerun_handler
|
from documents.signals.handlers import task_prerun_handler
|
||||||
from documents.signals.handlers import task_revoked_handler
|
from documents.signals.handlers import task_revoked_handler
|
||||||
from documents.tests.factories import PaperlessTaskFactory
|
from paperless_testing.factories import PaperlessTaskFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -34,8 +35,8 @@ def consume_input_doc():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def consume_overrides(django_user_model):
|
def consume_overrides():
|
||||||
user = django_user_model.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
overrides = mock.MagicMock(spec=DocumentMetadataOverrides)
|
overrides = mock.MagicMock(spec=DocumentMetadataOverrides)
|
||||||
overrides.owner_id = user.id
|
overrides.owner_id = user.id
|
||||||
return overrides
|
return overrides
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from documents.models import WorkflowAction
|
|||||||
from documents.sanity_checker import SanityCheckFailedException
|
from documents.sanity_checker import SanityCheckFailedException
|
||||||
from documents.sanity_checker import SanityCheckMessages
|
from documents.sanity_checker import SanityCheckMessages
|
||||||
from documents.tests.test_classifier import dummy_preprocess
|
from documents.tests.test_classifier import dummy_preprocess
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ from documents.conditionals import preview_etag
|
|||||||
from documents.conditionals import thumbnail_etag
|
from documents.conditionals import thumbnail_etag
|
||||||
from documents.conditionals import thumbnail_last_modified
|
from documents.conditionals import thumbnail_last_modified
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.versioning import resolve_effective_document_by_pk
|
from documents.versioning import resolve_effective_document_by_pk
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from django.test import TestCase
|
|||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from documents.caching import get_llm_suggestion_cache
|
from documents.caching import get_llm_suggestion_cache
|
||||||
@@ -29,11 +28,14 @@ from documents.models import StoragePath
|
|||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import UiSettings
|
from documents.models import UiSettings
|
||||||
from documents.signals.handlers import update_llm_suggestions_cache
|
from documents.signals.handlers import update_llm_suggestions_cache
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import read_streaming_response
|
from documents.tests.utils import read_streaming_response
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
from paperless_ai.exceptions import LLMProviderError
|
from paperless_ai.exceptions import LLMProviderError
|
||||||
from paperless_ai.exceptions import LLMTimeoutError
|
from paperless_ai.exceptions import LLMTimeoutError
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestViews(DirectoriesMixin, TestCase):
|
class TestViews(DirectoriesMixin, TestCase):
|
||||||
@@ -43,7 +45,7 @@ class TestViews(DirectoriesMixin, TestCase):
|
|||||||
ApplicationConfiguration.objects.get_or_create()
|
ApplicationConfiguration.objects.get_or_create()
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.create_user("testuser")
|
self.user = UserFactory(username="testuser")
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def test_login_redirect(self) -> None:
|
def test_login_redirect(self) -> None:
|
||||||
@@ -141,9 +143,7 @@ class TestViews(DirectoriesMixin, TestCase):
|
|||||||
codename__contains="sharelink",
|
codename__contains="sharelink",
|
||||||
)
|
)
|
||||||
self.user.user_permissions.add(*sharelink_permissions)
|
self.user.user_permissions.add(*sharelink_permissions)
|
||||||
self.user.user_permissions.add(
|
grant_global(self.user, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
@@ -205,9 +205,7 @@ class TestViews(DirectoriesMixin, TestCase):
|
|||||||
codename__contains="sharelink",
|
codename__contains="sharelink",
|
||||||
)
|
)
|
||||||
self.user.user_permissions.add(*sharelink_permissions)
|
self.user.user_permissions.add(*sharelink_permissions)
|
||||||
self.user.user_permissions.add(
|
grant_global(self.user, "view_document")
|
||||||
Permission.objects.get(codename="view_document"),
|
|
||||||
)
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
create_response = self.client.post(
|
create_response = self.client.post(
|
||||||
@@ -240,16 +238,16 @@ class TestViews(DirectoriesMixin, TestCase):
|
|||||||
group2 = Group.objects.create(name="group2")
|
group2 = Group.objects.create(name="group2")
|
||||||
group3 = Group.objects.create(name="group3")
|
group3 = Group.objects.create(name="group3")
|
||||||
t1 = Tag.objects.create(name="invoice", pk=1)
|
t1 = Tag.objects.create(name="invoice", pk=1)
|
||||||
assign_perm("view_tag", self.user, t1)
|
grant_object(self.user, t1, "view_tag")
|
||||||
assign_perm("view_tag", user2, t1)
|
grant_object(user2, t1, "view_tag")
|
||||||
assign_perm("view_tag", user3, t1)
|
grant_object(user3, t1, "view_tag")
|
||||||
assign_perm("view_tag", group1, t1)
|
grant_object(group1, t1, "view_tag")
|
||||||
assign_perm("view_tag", group2, t1)
|
grant_object(group2, t1, "view_tag")
|
||||||
assign_perm("view_tag", group3, t1)
|
grant_object(group3, t1, "view_tag")
|
||||||
assign_perm("change_tag", self.user, t1)
|
grant_object(self.user, t1, "change_tag")
|
||||||
assign_perm("change_tag", user2, t1)
|
grant_object(user2, t1, "change_tag")
|
||||||
assign_perm("change_tag", group1, t1)
|
grant_object(group1, t1, "change_tag")
|
||||||
assign_perm("change_tag", group2, t1)
|
grant_object(group2, t1, "change_tag")
|
||||||
|
|
||||||
Tag.objects.create(name="bank statement", pk=2)
|
Tag.objects.create(name="bank statement", pk=2)
|
||||||
d1 = Document.objects.create(
|
d1 = Document.objects.create(
|
||||||
@@ -338,7 +336,7 @@ class TestViews(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
class TestAISuggestions(DirectoriesMixin, TestCase):
|
class TestAISuggestions(DirectoriesMixin, TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.create_superuser(username="testuser")
|
self.user = UserFactory(username="testuser", superuser=True)
|
||||||
self.document = Document.objects.create(
|
self.document = Document.objects.create(
|
||||||
title="Test Document",
|
title="Test Document",
|
||||||
filename="test.pdf",
|
filename="test.pdf",
|
||||||
@@ -425,14 +423,10 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
|
|||||||
requester; the invisible tag id does not leak into either the
|
requester; the invisible tag id does not leak into either the
|
||||||
matched or suggested tags
|
matched or suggested tags
|
||||||
"""
|
"""
|
||||||
tag_owner = User.objects.create_user(username="cache_tag_owner")
|
tag_owner = UserFactory(username="cache_tag_owner")
|
||||||
invisible_tag = Tag.objects.create(name="cache_restricted", owner=tag_owner)
|
invisible_tag = Tag.objects.create(name="cache_restricted", owner=tag_owner)
|
||||||
requester = User.objects.create_user(username="cache_requester")
|
requester = UserFactory(username="cache_requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "view_document", "change_document", "view_tag")
|
||||||
*Permission.objects.filter(
|
|
||||||
codename__in=["view_document", "change_document", "view_tag"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
mock_get_cache.return_value = MagicMock(
|
mock_get_cache.return_value = MagicMock(
|
||||||
suggestions={
|
suggestions={
|
||||||
"title": "Untitled",
|
"title": "Untitled",
|
||||||
@@ -637,7 +631,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
|
|||||||
- The classification runs with the second user's visibility
|
- The classification runs with the second user's visibility
|
||||||
context without evicting the first user's result
|
context without evicting the first user's result
|
||||||
"""
|
"""
|
||||||
second_user = User.objects.create_superuser(username="second_user")
|
second_user = UserFactory(username="second_user", superuser=True)
|
||||||
empty_choices = {
|
empty_choices = {
|
||||||
"tags": {"existing_ids": [], "new_names": []},
|
"tags": {"existing_ids": [], "new_names": []},
|
||||||
"correspondents": {"existing_ids": [], "new_names": []},
|
"correspondents": {"existing_ids": [], "new_names": []},
|
||||||
@@ -875,14 +869,10 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
|
|||||||
permission filtering survives the full request path
|
permission filtering survives the full request path
|
||||||
- it does not appear in either the matched or suggested tags
|
- it does not appear in either the matched or suggested tags
|
||||||
"""
|
"""
|
||||||
tag_owner = User.objects.create_user(username="tagowner")
|
tag_owner = UserFactory(username="tagowner")
|
||||||
invisible_tag = Tag.objects.create(name="restricted", owner=tag_owner)
|
invisible_tag = Tag.objects.create(name="restricted", owner=tag_owner)
|
||||||
requester = User.objects.create_user(username="requester")
|
requester = UserFactory(username="requester")
|
||||||
requester.user_permissions.add(
|
grant_global(requester, "view_document", "change_document", "view_tag")
|
||||||
*Permission.objects.filter(
|
|
||||||
codename__in=["view_document", "change_document", "view_tag"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_get_ai_classification.return_value = {
|
mock_get_ai_classification.return_value = {
|
||||||
"title": "Untitled",
|
"title": "Untitled",
|
||||||
@@ -956,7 +946,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase):
|
|||||||
ENDPOINT = "/api/documents/chat/"
|
ENDPOINT = "/api/documents/chat/"
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.create_user(username="testuser", password="pass")
|
self.user = UserFactory(username="testuser", password="pass")
|
||||||
self.client.force_login(user=self.user)
|
self.client.force_login(user=self.user)
|
||||||
self.document = Document.objects.create(
|
self.document = Document.objects.create(
|
||||||
title="Test Document",
|
title="Test Document",
|
||||||
@@ -966,9 +956,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def grant_view_document_permission(self) -> None:
|
def grant_view_document_permission(self) -> None:
|
||||||
self.user.user_permissions.add(
|
grant_global(self.user, "view_document")
|
||||||
*Permission.objects.filter(codename="view_document"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(AI_ENABLED=False)
|
@override_settings(AI_ENABLED=False)
|
||||||
def test_post_ai_disabled(self) -> None:
|
def test_post_ai_disabled(self) -> None:
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from django.contrib.auth.models import User
|
|||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from guardian.shortcuts import get_groups_with_perms
|
from guardian.shortcuts import get_groups_with_perms
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from httpx import ConnectError
|
from httpx import ConnectError
|
||||||
@@ -64,13 +63,15 @@ from documents.models import WorkflowTrigger
|
|||||||
from documents.plugins.base import StopConsumeTaskError
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
from documents.serialisers import WorkflowTriggerSerializer
|
from documents.serialisers import WorkflowTriggerSerializer
|
||||||
from documents.signals import document_consumption_finished
|
from documents.signals import document_consumption_finished
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import DummyProgressManager
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
from documents.workflows.actions import execute_password_removal_action
|
from documents.workflows.actions import execute_password_removal_action
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
class TestWorkflows(
|
class TestWorkflows(
|
||||||
@@ -1952,7 +1953,7 @@ class TestWorkflows(
|
|||||||
original_filename="sample.pdf",
|
original_filename="sample.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -1982,7 +1983,7 @@ class TestWorkflows(
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SAMPLE_DIR / "simple.pdf",
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
@@ -2040,7 +2041,7 @@ class TestWorkflows(
|
|||||||
value_text="existing value",
|
value_text="existing value",
|
||||||
)
|
)
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -2084,7 +2085,7 @@ class TestWorkflows(
|
|||||||
)
|
)
|
||||||
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -2129,12 +2130,12 @@ class TestWorkflows(
|
|||||||
original_filename="sample.pdf",
|
original_filename="sample.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_perm("documents.view_document", self.user2, doc)
|
grant_object(self.user2, doc, "documents.view_document")
|
||||||
assign_perm("documents.change_document", self.user2, doc)
|
grant_object(self.user2, doc, "documents.change_document")
|
||||||
assign_perm("documents.view_document", self.group1, doc)
|
grant_object(self.group1, doc, "documents.view_document")
|
||||||
assign_perm("documents.change_document", self.group1, doc)
|
grant_object(self.group1, doc, "documents.change_document")
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -2900,12 +2901,12 @@ class TestWorkflows(
|
|||||||
doc.tags.set([self.t1, self.t2])
|
doc.tags.set([self.t1, self.t2])
|
||||||
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
||||||
doc.save()
|
doc.save()
|
||||||
assign_perm("documents.view_document", self.user3, doc)
|
grant_object(self.user3, doc, "documents.view_document")
|
||||||
assign_perm("documents.change_document", self.user3, doc)
|
grant_object(self.user3, doc, "documents.change_document")
|
||||||
assign_perm("documents.view_document", self.group1, doc)
|
grant_object(self.group1, doc, "documents.view_document")
|
||||||
assign_perm("documents.change_document", self.group1, doc)
|
grant_object(self.group1, doc, "documents.change_document")
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -2977,7 +2978,7 @@ class TestWorkflows(
|
|||||||
doc.refresh_from_db()
|
doc.refresh_from_db()
|
||||||
doc.tags.set([self.t1, self.t2])
|
doc.tags.set([self.t1, self.t2])
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
f"/api/documents/{doc.id}/",
|
f"/api/documents/{doc.id}/",
|
||||||
@@ -3041,7 +3042,7 @@ class TestWorkflows(
|
|||||||
doc.refresh_from_db()
|
doc.refresh_from_db()
|
||||||
doc.tags.set([self.t1])
|
doc.tags.set([self.t1])
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
f"/api/documents/{doc.id}/",
|
f"/api/documents/{doc.id}/",
|
||||||
@@ -3097,12 +3098,12 @@ class TestWorkflows(
|
|||||||
doc.tags.set([self.t1, self.t2])
|
doc.tags.set([self.t1, self.t2])
|
||||||
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
||||||
doc.save()
|
doc.save()
|
||||||
assign_perm("documents.view_document", self.user3, doc)
|
grant_object(self.user3, doc, "documents.view_document")
|
||||||
assign_perm("documents.change_document", self.user3, doc)
|
grant_object(self.user3, doc, "documents.change_document")
|
||||||
assign_perm("documents.view_document", self.group1, doc)
|
grant_object(self.group1, doc, "documents.view_document")
|
||||||
assign_perm("documents.change_document", self.group1, doc)
|
grant_object(self.group1, doc, "documents.change_document")
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -3395,7 +3396,7 @@ class TestWorkflows(
|
|||||||
original_filename="sample.pdf",
|
original_filename="sample.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
@@ -5357,7 +5358,7 @@ class TestDateWorkflowLocalization(
|
|||||||
c = Correspondent.objects.create(name="Correspondent Name")
|
c = Correspondent.objects.create(name="Correspondent Name")
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
superuser = User.objects.create_superuser("superuser")
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
client.force_authenticate(user=superuser)
|
client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
trigger = WorkflowTrigger.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
|||||||
+68
-125
@@ -1,8 +1,5 @@
|
|||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
from collections import namedtuple
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
@@ -19,7 +16,6 @@ from django.db import connection
|
|||||||
from django.db.migrations.executor import MigrationExecutor
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
from django.http import StreamingHttpResponse
|
from django.http import StreamingHttpResponse
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
from django.test import override_settings
|
|
||||||
|
|
||||||
from documents.consumer import AsnCheckPlugin
|
from documents.consumer import AsnCheckPlugin
|
||||||
from documents.consumer import ConsumerPlugin
|
from documents.consumer import ConsumerPlugin
|
||||||
@@ -31,65 +27,6 @@ from documents.parsers import ParseError
|
|||||||
from documents.plugins.helpers import ProgressStatusOptions
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
|
|
||||||
|
|
||||||
def setup_directories():
|
|
||||||
dirs = namedtuple("Dirs", ())
|
|
||||||
|
|
||||||
dirs.data_dir = Path(tempfile.mkdtemp()).resolve()
|
|
||||||
dirs.scratch_dir = Path(tempfile.mkdtemp()).resolve()
|
|
||||||
dirs.media_dir = Path(tempfile.mkdtemp()).resolve()
|
|
||||||
dirs.consumption_dir = Path(tempfile.mkdtemp()).resolve()
|
|
||||||
dirs.static_dir = Path(tempfile.mkdtemp()).resolve()
|
|
||||||
dirs.index_dir = dirs.data_dir / "index"
|
|
||||||
dirs.originals_dir = dirs.media_dir / "documents" / "originals"
|
|
||||||
dirs.thumbnail_dir = dirs.media_dir / "documents" / "thumbnails"
|
|
||||||
dirs.archive_dir = dirs.media_dir / "documents" / "archive"
|
|
||||||
dirs.logging_dir = dirs.data_dir / "log"
|
|
||||||
|
|
||||||
dirs.index_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
dirs.originals_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
dirs.thumbnail_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
dirs.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
dirs.logging_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
dirs.settings_override = override_settings(
|
|
||||||
DATA_DIR=dirs.data_dir,
|
|
||||||
SCRATCH_DIR=dirs.scratch_dir,
|
|
||||||
MEDIA_ROOT=dirs.media_dir,
|
|
||||||
ORIGINALS_DIR=dirs.originals_dir,
|
|
||||||
THUMBNAIL_DIR=dirs.thumbnail_dir,
|
|
||||||
ARCHIVE_DIR=dirs.archive_dir,
|
|
||||||
CONSUMPTION_DIR=dirs.consumption_dir,
|
|
||||||
LOGGING_DIR=dirs.logging_dir,
|
|
||||||
INDEX_DIR=dirs.index_dir,
|
|
||||||
STATIC_ROOT=dirs.static_dir,
|
|
||||||
MODEL_FILE=dirs.data_dir / "classification_model.pickle",
|
|
||||||
MEDIA_LOCK=dirs.media_dir / "media.lock",
|
|
||||||
)
|
|
||||||
dirs.settings_override.enable()
|
|
||||||
|
|
||||||
return dirs
|
|
||||||
|
|
||||||
|
|
||||||
def remove_dirs(dirs) -> None:
|
|
||||||
shutil.rmtree(dirs.media_dir, ignore_errors=True)
|
|
||||||
shutil.rmtree(dirs.data_dir, ignore_errors=True)
|
|
||||||
shutil.rmtree(dirs.scratch_dir, ignore_errors=True)
|
|
||||||
shutil.rmtree(dirs.consumption_dir, ignore_errors=True)
|
|
||||||
shutil.rmtree(dirs.static_dir, ignore_errors=True)
|
|
||||||
dirs.settings_override.disable()
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def paperless_environment():
|
|
||||||
dirs = None
|
|
||||||
try:
|
|
||||||
dirs = setup_directories()
|
|
||||||
yield dirs
|
|
||||||
finally:
|
|
||||||
if dirs:
|
|
||||||
remove_dirs(dirs)
|
|
||||||
|
|
||||||
|
|
||||||
def util_call_with_backoff(
|
def util_call_with_backoff(
|
||||||
method_or_callable: Callable,
|
method_or_callable: Callable,
|
||||||
args: list | tuple,
|
args: list | tuple,
|
||||||
@@ -158,27 +95,6 @@ def read_streaming_response(response: StreamingHttpResponse) -> bytes:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
class DirectoriesMixin:
|
|
||||||
"""
|
|
||||||
Creates and overrides settings for all folders and paths, then ensures
|
|
||||||
they are cleaned up on exit
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
from documents.search import reset_backend
|
|
||||||
|
|
||||||
reset_backend()
|
|
||||||
self.dirs = setup_directories()
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
from documents.search import reset_backend
|
|
||||||
|
|
||||||
super().tearDown()
|
|
||||||
reset_backend()
|
|
||||||
remove_dirs(self.dirs)
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystemAssertsMixin:
|
class FileSystemAssertsMixin:
|
||||||
"""
|
"""
|
||||||
Utilities for checks various state information of the file system
|
Utilities for checks various state information of the file system
|
||||||
@@ -196,20 +112,6 @@ class FileSystemAssertsMixin:
|
|||||||
def assertIsNotDir(self, path: PathLike[str] | str) -> None:
|
def assertIsNotDir(self, path: PathLike[str] | str) -> None:
|
||||||
self.assertFalse(Path(path).resolve().is_dir(), f"Dir does exist: {path}")
|
self.assertFalse(Path(path).resolve().is_dir(), f"Dir does exist: {path}")
|
||||||
|
|
||||||
def assertFilesEqual(
|
|
||||||
self,
|
|
||||||
path1: PathLike[str] | str,
|
|
||||||
path2: PathLike[str] | str,
|
|
||||||
) -> None:
|
|
||||||
path1 = Path(path1)
|
|
||||||
path2 = Path(path2)
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
hash1 = hashlib.sha256(path1.read_bytes()).hexdigest()
|
|
||||||
hash2 = hashlib.sha256(path2.read_bytes()).hexdigest()
|
|
||||||
|
|
||||||
self.assertEqual(hash1, hash2, "File SHA256 mismatch")
|
|
||||||
|
|
||||||
def assertFileCountInDir(self, path: PathLike[str] | str, count: int) -> None:
|
def assertFileCountInDir(self, path: PathLike[str] | str, count: int) -> None:
|
||||||
path = Path(path).resolve()
|
path = Path(path).resolve()
|
||||||
self.assertTrue(path.is_dir(), f"Path {path} is not a directory")
|
self.assertTrue(path.is_dir(), f"Path {path} is not a directory")
|
||||||
@@ -221,24 +123,6 @@ class FileSystemAssertsMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsumerProgressMixin:
|
|
||||||
"""
|
|
||||||
Mocks the Consumer _send_progress, preventing attempts to connect to Redis
|
|
||||||
and allowing access to its calls for verification
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.send_progress_patcher = mock.patch(
|
|
||||||
"documents.consumer.Consumer._send_progress",
|
|
||||||
)
|
|
||||||
self.send_progress_mock = self.send_progress_patcher.start()
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
self.send_progress_patcher.stop()
|
|
||||||
|
|
||||||
|
|
||||||
class ConsumeTaskMixin:
|
class ConsumeTaskMixin:
|
||||||
"""
|
"""
|
||||||
Provides mocking of the consume_file asynchronous task and useful utilities
|
Provides mocking of the consume_file asynchronous task and useful utilities
|
||||||
@@ -275,6 +159,16 @@ class ConsumeTaskMixin:
|
|||||||
|
|
||||||
|
|
||||||
class TestMigrations(TransactionTestCase):
|
class TestMigrations(TransactionTestCase):
|
||||||
|
"""Run a migration on seeded data, then let the tests inspect the result.
|
||||||
|
|
||||||
|
By default every test migrates back, seeds, migrates forward and returns to
|
||||||
|
the latest migration, which costs several seconds. A class whose tests only
|
||||||
|
read the migrated data can set ``migrate_once`` to pay that once per class:
|
||||||
|
the migration runs for the first test, the database is left alone between
|
||||||
|
tests, and it is restored and flushed when the class finishes. Such tests
|
||||||
|
must not write to the database.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app(self):
|
def app(self):
|
||||||
return apps.get_containing_app_config(type(self).__module__).name
|
return apps.get_containing_app_config(type(self).__module__).name
|
||||||
@@ -282,11 +176,38 @@ class TestMigrations(TransactionTestCase):
|
|||||||
migrate_from = None
|
migrate_from = None
|
||||||
dependencies = None
|
dependencies = None
|
||||||
migrate_to = None
|
migrate_to = None
|
||||||
auto_migrate = True
|
migrate_once = False
|
||||||
|
|
||||||
|
_once_owner: "TestMigrations | None" = None
|
||||||
|
_once_state: dict[str, Any] | None = None
|
||||||
|
_once_finishing = False
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
cls = type(self)
|
||||||
|
if self.migrate_once:
|
||||||
|
if cls._once_state is not None:
|
||||||
|
vars(self).update(cls._once_state)
|
||||||
|
return
|
||||||
|
if cls._once_owner is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"The migration in '{cls.__name__}' failed for an earlier test",
|
||||||
|
)
|
||||||
|
# Recorded before migrating so a failed migration is still restored
|
||||||
|
cls._once_owner = self
|
||||||
|
before = dict(vars(self))
|
||||||
|
|
||||||
|
self._migrate()
|
||||||
|
|
||||||
|
if self.migrate_once:
|
||||||
|
cls._once_state = {
|
||||||
|
name: value
|
||||||
|
for name, value in vars(self).items()
|
||||||
|
if name not in before or before[name] is not value
|
||||||
|
}
|
||||||
|
|
||||||
|
def _migrate(self) -> None:
|
||||||
assert self.migrate_from and self.migrate_to, (
|
assert self.migrate_from and self.migrate_to, (
|
||||||
f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties"
|
f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties"
|
||||||
)
|
)
|
||||||
@@ -304,10 +225,6 @@ class TestMigrations(TransactionTestCase):
|
|||||||
|
|
||||||
self.apps = old_apps
|
self.apps = old_apps
|
||||||
|
|
||||||
if self.auto_migrate:
|
|
||||||
self.performMigration()
|
|
||||||
|
|
||||||
def performMigration(self) -> None:
|
|
||||||
# Run the migration to test
|
# Run the migration to test
|
||||||
executor = MigrationExecutor(connection)
|
executor = MigrationExecutor(connection)
|
||||||
executor.loader.build_graph() # reload.
|
executor.loader.build_graph() # reload.
|
||||||
@@ -318,19 +235,45 @@ class TestMigrations(TransactionTestCase):
|
|||||||
def setUpBeforeMigration(self, apps) -> None:
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _migrate_to_latest(self) -> None:
|
||||||
|
executor = MigrationExecutor(connection)
|
||||||
|
executor.loader.build_graph()
|
||||||
|
targets = executor.loader.graph.leaf_nodes()
|
||||||
|
executor.migrate(targets)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure the database schema is restored to the latest migration after
|
Ensure the database schema is restored to the latest migration after
|
||||||
each migration test, so subsequent tests run against HEAD.
|
each migration test, so subsequent tests run against HEAD.
|
||||||
"""
|
"""
|
||||||
|
if self.migrate_once and not self._once_finishing:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
executor = MigrationExecutor(connection)
|
self._migrate_to_latest()
|
||||||
executor.loader.build_graph()
|
|
||||||
targets = executor.loader.graph.leaf_nodes()
|
|
||||||
executor.migrate(targets)
|
|
||||||
finally:
|
finally:
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
def _fixture_teardown(self) -> None:
|
||||||
|
# Django flushes every table after each test, which would discard the
|
||||||
|
# data the remaining tests of a migrate_once class still need
|
||||||
|
if self.migrate_once and not self._once_finishing:
|
||||||
|
return
|
||||||
|
super()._fixture_teardown()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls) -> None:
|
||||||
|
owner = cls._once_owner
|
||||||
|
try:
|
||||||
|
if owner is not None:
|
||||||
|
cls._once_finishing = True
|
||||||
|
owner.tearDown()
|
||||||
|
owner._fixture_teardown()
|
||||||
|
finally:
|
||||||
|
cls._once_owner = None
|
||||||
|
cls._once_state = None
|
||||||
|
cls._once_finishing = False
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
|
||||||
class SampleDirMixin:
|
class SampleDirMixin:
|
||||||
SAMPLE_DIR = Path(__file__).parent / "samples"
|
SAMPLE_DIR = Path(__file__).parent / "samples"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
||||||
"PO-Revision-Date: 2026-09-18 01:30\n"
|
"PO-Revision-Date: 2026-09-19 12:26\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: German, Switzerland\n"
|
"Language-Team: German, Switzerland\n"
|
||||||
"Language: de_CH\n"
|
"Language: de_CH\n"
|
||||||
@@ -350,7 +350,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: documents/models.py:570
|
#: documents/models.py:570
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr ""
|
msgstr "Download"
|
||||||
|
|
||||||
#: documents/models.py:571
|
#: documents/models.py:571
|
||||||
msgid "Envelope"
|
msgid "Envelope"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
||||||
"PO-Revision-Date: 2026-09-18 01:30\n"
|
"PO-Revision-Date: 2026-09-20 00:58\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
||||||
"PO-Revision-Date: 2026-09-18 01:30\n"
|
"PO-Revision-Date: 2026-09-20 00:58\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Italian\n"
|
"Language-Team: Italian\n"
|
||||||
"Language: it_IT\n"
|
"Language: it_IT\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
||||||
"PO-Revision-Date: 2026-09-18 01:30\n"
|
"PO-Revision-Date: 2026-09-19 12:26\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Russian\n"
|
"Language-Team: Russian\n"
|
||||||
"Language: ru_RU\n"
|
"Language: ru_RU\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
||||||
"PO-Revision-Date: 2026-09-18 01:30\n"
|
"PO-Revision-Date: 2026-09-19 12:26\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Slovenian\n"
|
"Language-Team: Slovenian\n"
|
||||||
"Language: sl_SI\n"
|
"Language: sl_SI\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
"POT-Creation-Date: 2026-09-18 01:29+0000\n"
|
||||||
"PO-Revision-Date: 2026-09-18 18:55\n"
|
"PO-Revision-Date: 2026-09-20 12:26\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Swedish\n"
|
"Language-Team: Swedish\n"
|
||||||
"Language: sv_SE\n"
|
"Language: sv_SE\n"
|
||||||
|
|||||||
@@ -394,7 +394,11 @@ class RasterisedDocumentParser:
|
|||||||
|
|
||||||
plain_pdf_path = Path(self.tempdir) / "image_plain.pdf"
|
plain_pdf_path = Path(self.tempdir) / "image_plain.pdf"
|
||||||
try:
|
try:
|
||||||
convert_kwargs: dict = {}
|
convert_kwargs: dict = {
|
||||||
|
# Ignore invalid EXIF orientation values (e.g. 0) instead of
|
||||||
|
# aborting the conversion; valid values are still applied
|
||||||
|
"rotation": img2pdf.Rotation.ifvalid,
|
||||||
|
}
|
||||||
if self.settings.image_dpi is not None:
|
if self.settings.image_dpi is not None:
|
||||||
convert_kwargs["layout_fun"] = img2pdf.get_fixed_dpi_layout_fun(
|
convert_kwargs["layout_fun"] = img2pdf.get_fixed_dpi_layout_fun(
|
||||||
(self.settings.image_dpi, self.settings.image_dpi),
|
(self.settings.image_dpi, self.settings.image_dpi),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import img2pdf
|
|||||||
import magic
|
import magic
|
||||||
import pikepdf
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
|
|
||||||
@@ -139,3 +140,24 @@ class TestConvertImageToPdfa:
|
|||||||
tesseract_parser._convert_image_to_pdfa(simple_png_file)
|
tesseract_parser._convert_image_to_pdfa(simple_png_file)
|
||||||
|
|
||||||
spy.assert_not_called()
|
spy.assert_not_called()
|
||||||
|
|
||||||
|
def test_invalid_exif_orientation_is_ignored(
|
||||||
|
self,
|
||||||
|
tesseract_parser: RasterisedDocumentParser,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN: a JPEG with an invalid EXIF orientation value (0)
|
||||||
|
WHEN: _convert_image_to_pdfa is called
|
||||||
|
THEN: the invalid value is ignored and a valid PDF is produced
|
||||||
|
"""
|
||||||
|
image_path = tmp_path / "invalid_orientation.jpg"
|
||||||
|
with Image.new("RGB", (120, 80), "white") as image:
|
||||||
|
exif = image.getexif()
|
||||||
|
exif[274] = 0 # EXIF tag 274: Orientation
|
||||||
|
image.save(image_path, exif=exif)
|
||||||
|
|
||||||
|
result = tesseract_parser._convert_image_to_pdfa(image_path)
|
||||||
|
|
||||||
|
assert result.exists()
|
||||||
|
assert magic.from_file(str(result), mime=True) == "application/pdf"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import json
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
from paperless.models import CleanChoices
|
from paperless.models import CleanChoices
|
||||||
@@ -11,6 +10,7 @@ from paperless.models import ColorConvertChoices
|
|||||||
from paperless.models import ModeChoices
|
from paperless.models import ModeChoices
|
||||||
from paperless.models import OutputTypeChoices
|
from paperless.models import OutputTypeChoices
|
||||||
from paperless.parsers.tesseract import RasterisedDocumentParser
|
from paperless.parsers.tesseract import RasterisedDocumentParser
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
class TestParserSettingsFromDb(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
class TestParserSettingsFromDb(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
|
|||||||
@@ -2,22 +2,20 @@ import os
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
|
||||||
from paperless.settings import _parse_remote_user_settings
|
from paperless.settings import _parse_remote_user_settings
|
||||||
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestRemoteUser(DirectoriesMixin, APITestCase):
|
class TestRemoteUser(DirectoriesMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.user = User.objects.create_superuser(
|
self.user = UserFactory(username="temp_admin", superuser=True)
|
||||||
username="temp_admin",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_remote_user(self) -> None:
|
def test_remote_user(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from pytest_mock import MockerFixture
|
|||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from paperless.adapter import DrfTokenStrategy
|
from paperless.adapter import DrfTokenStrategy
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -25,7 +26,7 @@ class TestCustomAccountAdapter:
|
|||||||
# With no accounts, signups should be allowed
|
# With no accounts, signups should be allowed
|
||||||
assert adapter.is_open_for_signup(None)
|
assert adapter.is_open_for_signup(None)
|
||||||
|
|
||||||
User.objects.create_user("testuser")
|
UserFactory(username="testuser")
|
||||||
|
|
||||||
settings.ACCOUNT_ALLOW_SIGNUPS = True
|
settings.ACCOUNT_ALLOW_SIGNUPS = True
|
||||||
assert adapter.is_open_for_signup(None)
|
assert adapter.is_open_for_signup(None)
|
||||||
@@ -92,7 +93,7 @@ class TestCustomAccountAdapter:
|
|||||||
) -> None:
|
) -> None:
|
||||||
settings.ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"]
|
settings.ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"]
|
||||||
Group.objects.create(name="group1")
|
Group.objects.create(name="group1")
|
||||||
user = User.objects.create_user("testuser")
|
user = UserFactory(username="testuser")
|
||||||
adapter = get_adapter()
|
adapter = get_adapter()
|
||||||
form = mocker.MagicMock(
|
form = mocker.MagicMock(
|
||||||
cleaned_data={
|
cleaned_data={
|
||||||
@@ -152,7 +153,7 @@ class TestCustomSocialAccountAdapter:
|
|||||||
settings.SOCIAL_ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"]
|
settings.SOCIAL_ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"]
|
||||||
Group.objects.create(name="group1")
|
Group.objects.create(name="group1")
|
||||||
adapter = get_social_adapter()
|
adapter = get_social_adapter()
|
||||||
user = User.objects.create_user("testuser")
|
user = UserFactory(username="testuser")
|
||||||
sociallogin = mocker.MagicMock(user=user)
|
sociallogin = mocker.MagicMock(user=user)
|
||||||
|
|
||||||
user = adapter.save_user(HttpRequest(), sociallogin, None)
|
user = adapter.save_user(HttpRequest(), sociallogin, None)
|
||||||
@@ -187,7 +188,7 @@ class TestDrfTokenStrategy:
|
|||||||
THEN:
|
THEN:
|
||||||
- A new token is created and its key is returned
|
- A new token is created and its key is returned
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user("testuser")
|
user = UserFactory(username="testuser")
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.user = user
|
request.user = user
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ class TestDrfTokenStrategy:
|
|||||||
THEN:
|
THEN:
|
||||||
- The same token key is returned (no new token created)
|
- The same token key is returned (no new token created)
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user("testuser")
|
user = UserFactory(username="testuser")
|
||||||
existing_token = Token.objects.create(user=user)
|
existing_token = Token.objects.create(user=user)
|
||||||
|
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestApiAuthViews(TestCase):
|
class TestApiAuthViews(TestCase):
|
||||||
def test_api_auth_login_uses_allauth_login_view(self):
|
def test_api_auth_login_uses_allauth_login_view(self):
|
||||||
@@ -24,10 +25,7 @@ class TestApiAuthViews(TestCase):
|
|||||||
@override_settings(DISABLE_REGULAR_LOGIN=True)
|
@override_settings(DISABLE_REGULAR_LOGIN=True)
|
||||||
def test_api_auth_login_respects_disable_regular_login(self):
|
def test_api_auth_login_respects_disable_regular_login(self):
|
||||||
username = f"testuser-{uuid.uuid4().hex}"
|
username = f"testuser-{uuid.uuid4().hex}"
|
||||||
User.objects.create_user(
|
UserFactory(username=username, password="testpassword")
|
||||||
username=username,
|
|
||||||
password="testpassword",
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("rest_framework:login"),
|
reverse("rest_framework:login"),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from paperless.auth import AutoLoginMiddleware
|
from paperless.auth import AutoLoginMiddleware
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
@override_settings(AUTO_LOGIN_USERNAME="autologin")
|
@override_settings(AUTO_LOGIN_USERNAME="autologin")
|
||||||
@@ -29,7 +29,7 @@ class TestAutoLoginMiddleware(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- That user is attached to the request
|
- That user is attached to the request
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(username="autologin")
|
user = UserFactory(username="autologin")
|
||||||
|
|
||||||
request = self._process(self.factory.get("/"))
|
request = self._process(self.factory.get("/"))
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class TestAutoLoginMiddleware(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- The request is left anonymous rather than authenticated as them
|
- The request is left anonymous rather than authenticated as them
|
||||||
"""
|
"""
|
||||||
User.objects.create_user(username="autologin", is_active=False)
|
UserFactory(username="autologin", is_active=False)
|
||||||
|
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@@ -19,35 +18,7 @@ from paperless.checks import check_v3_minimum_upgrade_version
|
|||||||
from paperless.checks import debug_mode_check
|
from paperless.checks import debug_mode_check
|
||||||
from paperless.checks import paths_check
|
from paperless.checks import paths_check
|
||||||
from paperless.checks import settings_values_check
|
from paperless.checks import settings_values_check
|
||||||
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class PaperlessTestDirs:
|
|
||||||
data_dir: Path
|
|
||||||
media_dir: Path
|
|
||||||
consumption_dir: Path
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: consolidate with documents/tests/conftest.py PaperlessDirs/paperless_dirs
|
|
||||||
# once the paperless and documents test suites are ready to share fixtures.
|
|
||||||
@pytest.fixture()
|
|
||||||
def directories(tmp_path: Path, settings: Settings) -> PaperlessTestDirs:
|
|
||||||
data_dir = tmp_path / "data"
|
|
||||||
media_dir = tmp_path / "media"
|
|
||||||
consumption_dir = tmp_path / "consumption"
|
|
||||||
|
|
||||||
for d in (data_dir, media_dir, consumption_dir):
|
|
||||||
d.mkdir()
|
|
||||||
|
|
||||||
settings.DATA_DIR = data_dir
|
|
||||||
settings.MEDIA_ROOT = media_dir
|
|
||||||
settings.CONSUMPTION_DIR = consumption_dir
|
|
||||||
|
|
||||||
return PaperlessTestDirs(
|
|
||||||
data_dir=data_dir,
|
|
||||||
media_dir=media_dir,
|
|
||||||
consumption_dir=consumption_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestChecks:
|
class TestChecks:
|
||||||
@@ -58,7 +29,7 @@ class TestChecks:
|
|||||||
settings.CONVERT_BINARY = "uuuhh"
|
settings.CONVERT_BINARY = "uuuhh"
|
||||||
assert len(binaries_check(None)) == 1
|
assert len(binaries_check(None)) == 1
|
||||||
|
|
||||||
@pytest.mark.usefixtures("directories")
|
@pytest.mark.usefixtures("paperless_dirs")
|
||||||
def test_paths_check(self) -> None:
|
def test_paths_check(self) -> None:
|
||||||
assert paths_check(None) == []
|
assert paths_check(None) == []
|
||||||
|
|
||||||
@@ -73,17 +44,17 @@ class TestChecks:
|
|||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
assert msg.msg.endswith("is set but doesn't exist.")
|
assert msg.msg.endswith("is set but doesn't exist.")
|
||||||
|
|
||||||
def test_paths_check_no_access(self, directories: PaperlessTestDirs) -> None:
|
def test_paths_check_no_access(self, paperless_dirs: PaperlessDirs) -> None:
|
||||||
directories.data_dir.chmod(0o000)
|
paperless_dirs.data_dir.chmod(0o000)
|
||||||
directories.media_dir.chmod(0o000)
|
paperless_dirs.media_dir.chmod(0o000)
|
||||||
directories.consumption_dir.chmod(0o000)
|
paperless_dirs.consumption_dir.chmod(0o000)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msgs = paths_check(None)
|
msgs = paths_check(None)
|
||||||
finally:
|
finally:
|
||||||
directories.data_dir.chmod(0o777)
|
paperless_dirs.data_dir.chmod(0o777)
|
||||||
directories.media_dir.chmod(0o777)
|
paperless_dirs.media_dir.chmod(0o777)
|
||||||
directories.consumption_dir.chmod(0o777)
|
paperless_dirs.consumption_dir.chmod(0o777)
|
||||||
|
|
||||||
assert len(msgs) == 3
|
assert len(msgs) == 3
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from documents.tests.utils import TestMigrations
|
|||||||
class TestMigrateSkipArchiveFile(TestMigrations):
|
class TestMigrateSkipArchiveFile(TestMigrations):
|
||||||
migrate_from = "0007_optimize_integer_field_sizes"
|
migrate_from = "0007_optimize_integer_field_sizes"
|
||||||
migrate_to = "0008_replace_skip_archive_file"
|
migrate_to = "0008_replace_skip_archive_file"
|
||||||
|
migrate_once = True
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps):
|
def setUpBeforeMigration(self, apps):
|
||||||
ApplicationConfiguration = apps.get_model(
|
ApplicationConfiguration = apps.get_model(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
@@ -9,6 +8,7 @@ from django.test import override_settings
|
|||||||
from documents.models import UiSettings
|
from documents.models import UiSettings
|
||||||
from paperless.signals import handle_failed_login
|
from paperless.signals import handle_failed_login
|
||||||
from paperless.signals import handle_social_account_updated
|
from paperless.signals import handle_social_account_updated
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestFailedLoginLogging(TestCase):
|
class TestFailedLoginLogging(TestCase):
|
||||||
@@ -120,7 +120,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
- The user's groups are updated to match the social login's groups
|
- The user's groups are updated to match the social login's groups
|
||||||
"""
|
"""
|
||||||
group = Group.objects.create(name="group1")
|
group = Group.objects.create(name="group1")
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -147,7 +147,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
- The user's groups are not updated
|
- The user's groups are not updated
|
||||||
"""
|
"""
|
||||||
Group.objects.create(name="group1")
|
Group.objects.create(name="group1")
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -180,7 +180,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
would be rejected for a deactivated user anyway
|
would be rejected for a deactivated user anyway
|
||||||
"""
|
"""
|
||||||
Group.objects.create(name="admin-group")
|
Group.objects.create(name="admin-group")
|
||||||
user = User.objects.create_user(
|
user = UserFactory(
|
||||||
username="inactive_user",
|
username="inactive_user",
|
||||||
is_active=False,
|
is_active=False,
|
||||||
is_superuser=False,
|
is_superuser=False,
|
||||||
@@ -215,7 +215,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
- The user's groups are cleared to match the social login's groups
|
- The user's groups are cleared to match the social login's groups
|
||||||
"""
|
"""
|
||||||
group = Group.objects.create(name="group1")
|
group = Group.objects.create(name="group1")
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
user.save()
|
user.save()
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
@@ -244,7 +244,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
- The user's groups are updated using `userinfo.groups`
|
- The user's groups are updated using `userinfo.groups`
|
||||||
"""
|
"""
|
||||||
group = Group.objects.create(name="group1")
|
group = Group.objects.create(name="group1")
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -275,7 +275,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
- The user's groups are updated using `id_token.groups`
|
- The user's groups are updated using `id_token.groups`
|
||||||
"""
|
"""
|
||||||
group = Group.objects.create(name="group1")
|
group = Group.objects.create(name="group1")
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -310,11 +310,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User is not promoted, since only an exact group match counts
|
- User is not promoted, since only an exact group match counts
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser", is_superuser=False, is_staff=False)
|
||||||
username="testuser",
|
|
||||||
is_superuser=False,
|
|
||||||
is_staff=False,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -345,11 +341,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User becomes superuser and staff
|
- User becomes superuser and staff
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser_s_e", is_superuser=False, is_staff=False)
|
||||||
username="testuser_s_e",
|
|
||||||
is_superuser=False,
|
|
||||||
is_staff=False,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -380,11 +372,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User loses superuser status but preserves staff status if they had it
|
- User loses superuser status but preserves staff status if they had it
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser_s_d", is_superuser=True, is_staff=True)
|
||||||
username="testuser_s_d",
|
|
||||||
is_superuser=True,
|
|
||||||
is_staff=True,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -415,11 +403,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User becomes staff
|
- User becomes staff
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser_st_e", is_superuser=False, is_staff=False)
|
||||||
username="testuser_st_e",
|
|
||||||
is_superuser=False,
|
|
||||||
is_staff=False,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -450,11 +434,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User loses staff status
|
- User loses staff status
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser_st_d", is_superuser=False, is_staff=True)
|
||||||
username="testuser_st_d",
|
|
||||||
is_superuser=False,
|
|
||||||
is_staff=True,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(
|
account=Mock(
|
||||||
@@ -485,11 +465,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
- Roles are correctly assigned/revoked according to groups
|
- Roles are correctly assigned/revoked according to groups
|
||||||
"""
|
"""
|
||||||
# Case 1: has both
|
# Case 1: has both
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser_b_1", is_superuser=False, is_staff=False)
|
||||||
username="testuser_b_1",
|
|
||||||
is_superuser=False,
|
|
||||||
is_staff=False,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(extra_data={"groups": ["admin-group", "staff-group"]}),
|
account=Mock(extra_data={"groups": ["admin-group", "staff-group"]}),
|
||||||
@@ -504,11 +480,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
self.assertTrue(user.is_staff)
|
self.assertTrue(user.is_staff)
|
||||||
|
|
||||||
# Case 2: has only staff
|
# Case 2: has only staff
|
||||||
user2 = User.objects.create_user(
|
user2 = UserFactory(username="testuser_b_2", is_superuser=True, is_staff=True)
|
||||||
username="testuser_b_2",
|
|
||||||
is_superuser=True,
|
|
||||||
is_staff=True,
|
|
||||||
)
|
|
||||||
sociallogin2 = Mock(
|
sociallogin2 = Mock(
|
||||||
user=user2,
|
user=user2,
|
||||||
account=Mock(extra_data={"groups": ["staff-group"]}),
|
account=Mock(extra_data={"groups": ["staff-group"]}),
|
||||||
@@ -523,11 +495,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
self.assertTrue(user2.is_staff)
|
self.assertTrue(user2.is_staff)
|
||||||
|
|
||||||
# Case 3: has neither
|
# Case 3: has neither
|
||||||
user3 = User.objects.create_user(
|
user3 = UserFactory(username="testuser_b_3", is_superuser=True, is_staff=True)
|
||||||
username="testuser_b_3",
|
|
||||||
is_superuser=True,
|
|
||||||
is_staff=True,
|
|
||||||
)
|
|
||||||
sociallogin3 = Mock(
|
sociallogin3 = Mock(
|
||||||
user=user3,
|
user=user3,
|
||||||
account=Mock(extra_data={"groups": ["other-group"]}),
|
account=Mock(extra_data={"groups": ["other-group"]}),
|
||||||
@@ -554,11 +522,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Existing roles are not modified
|
- Existing roles are not modified
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="testuser_n_s", is_superuser=True, is_staff=True)
|
||||||
username="testuser_n_s",
|
|
||||||
is_superuser=True,
|
|
||||||
is_staff=True,
|
|
||||||
)
|
|
||||||
sociallogin = Mock(
|
sociallogin = Mock(
|
||||||
user=user,
|
user=user,
|
||||||
account=Mock(extra_data={"groups": ["admin-group", "staff-group"]}),
|
account=Mock(extra_data={"groups": ["admin-group", "staff-group"]}),
|
||||||
@@ -586,7 +550,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User's superuser status is demoted, matching the group claim exactly
|
- User's superuser status is demoted, matching the group claim exactly
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(
|
||||||
username="local_admin",
|
username="local_admin",
|
||||||
password="password123",
|
password="password123",
|
||||||
is_superuser=True,
|
is_superuser=True,
|
||||||
@@ -618,11 +582,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User's superuser status is demoted, even though they are the last admin
|
- User's superuser status is demoted, even though they are the last admin
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(username="last_admin", is_superuser=True, is_staff=True)
|
||||||
username="last_admin",
|
|
||||||
is_superuser=True,
|
|
||||||
is_staff=True,
|
|
||||||
)
|
|
||||||
user.set_unusable_password()
|
user.set_unusable_password()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -652,7 +612,7 @@ class TestSyncSocialLoginGroups(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- User's staff status is demoted, matching the group claim exactly
|
- User's staff status is demoted, matching the group claim exactly
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(
|
user = UserFactory(
|
||||||
username="local_staff",
|
username="local_staff",
|
||||||
password="password123",
|
password="password123",
|
||||||
is_superuser=False,
|
is_superuser=False,
|
||||||
@@ -688,8 +648,8 @@ class TestUserGroupDeletionCleanup(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- References in ui_settings are cleaned up
|
- References in ui_settings are cleaned up
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
user2 = User.objects.create_user(username="testuser2")
|
user2 = UserFactory(username="testuser2")
|
||||||
group = Group.objects.create(name="testgroup")
|
group = Group.objects.create(name="testgroup")
|
||||||
|
|
||||||
ui_settings = UiSettings.objects.create(
|
ui_settings = UiSettings.objects.create(
|
||||||
@@ -727,8 +687,8 @@ class TestUserGroupDeletionCleanup(TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Error is logged and the system remains stable
|
- Error is logged and the system remains stable
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(username="testuser")
|
user = UserFactory(username="testuser")
|
||||||
user2 = User.objects.create_user(username="testuser2")
|
user2 = UserFactory(username="testuser2")
|
||||||
user2_id = user2.id
|
user2_id = user2.id
|
||||||
Group.objects.create(name="testgroup")
|
Group.objects.create(name="testgroup")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[tuple[int, int, int]] = (3, 2, 0)
|
__version__: Final[tuple[int, int, int]] = (3, 2, 1)
|
||||||
# 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
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_mock
|
import pytest_mock
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from guardian.shortcuts import remove_perm
|
from guardian.shortcuts import remove_perm
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.search import TantivyBackend
|
from documents.search import TantivyBackend
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.factories import TagFactory
|
|
||||||
from documents.tests.factories import UserFactory
|
|
||||||
from paperless.config import AIConfig
|
from paperless.config import AIConfig
|
||||||
from paperless_ai.ai_classifier import TAXONOMY_CANDIDATE_TOP_K
|
from paperless_ai.ai_classifier import TAXONOMY_CANDIDATE_TOP_K
|
||||||
from paperless_ai.ai_classifier import _fulltext_similar_documents
|
from paperless_ai.ai_classifier import _fulltext_similar_documents
|
||||||
@@ -27,6 +23,10 @@ from paperless_ai.ai_classifier import get_taxonomy_context
|
|||||||
from paperless_ai.taxonomy import SimilarDocument
|
from paperless_ai.taxonomy import SimilarDocument
|
||||||
from paperless_ai.taxonomy import TaxonomyCandidate
|
from paperless_ai.taxonomy import TaxonomyCandidate
|
||||||
from paperless_ai.taxonomy import TaxonomyCandidates
|
from paperless_ai.taxonomy import TaxonomyCandidates
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -815,8 +815,8 @@ class TestFulltextSimilarDocuments:
|
|||||||
content="shared content phrase",
|
content="shared content phrase",
|
||||||
owner=owner,
|
owner=owner,
|
||||||
)
|
)
|
||||||
assign_perm("view_document", viewer, permitted)
|
grant_object(viewer, permitted, "view_document")
|
||||||
assign_perm("view_document", viewer, now_private)
|
grant_object(viewer, now_private, "view_document")
|
||||||
fulltext_backend.add_or_update(source)
|
fulltext_backend.add_or_update(source)
|
||||||
fulltext_backend.add_or_update(permitted)
|
fulltext_backend.add_or_update(permitted)
|
||||||
fulltext_backend.add_or_update(now_private)
|
fulltext_backend.add_or_update(now_private)
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ from documents.models import Note
|
|||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.signals import document_consumption_finished
|
from documents.signals import document_consumption_finished
|
||||||
from documents.signals import document_updated
|
from documents.signals import document_updated
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.factories import PaperlessTaskFactory
|
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
from paperless_ai import indexing
|
from paperless_ai import indexing
|
||||||
from paperless_ai.tests.conftest import FakeEmbedding
|
from paperless_ai.tests.conftest import FakeEmbedding
|
||||||
from paperless_ai.vector_store import PaperlessSqliteVecVectorStore
|
from paperless_ai.vector_store import PaperlessSqliteVecVectorStore
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import PaperlessTaskFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from llama_index.core.embeddings.mock_embed_model import MockEmbedding
|
|||||||
from llama_index.core.schema import TextNode
|
from llama_index.core.schema import TextNode
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from paperless_ai import chat
|
from paperless_ai import chat
|
||||||
from paperless_ai import indexing
|
from paperless_ai import indexing
|
||||||
from paperless_ai.chat import CHAT_ERROR_MESSAGE
|
from paperless_ai.chat import CHAT_ERROR_MESSAGE
|
||||||
@@ -22,6 +21,7 @@ from paperless_ai.chat import CHAT_METADATA_DELIMITER
|
|||||||
from paperless_ai.chat import _build_chat_prompt
|
from paperless_ai.chat import _build_chat_prompt
|
||||||
from paperless_ai.chat import _build_refine_prompt
|
from paperless_ai.chat import _build_refine_prompt
|
||||||
from paperless_ai.chat import stream_chat_with_documents
|
from paperless_ai.chat import stream_chat_with_documents
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ from documents.models import Correspondent
|
|||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.factories import CorrespondentFactory
|
|
||||||
from documents.tests.factories import DocumentTypeFactory
|
|
||||||
from documents.tests.factories import StoragePathFactory
|
|
||||||
from documents.tests.factories import TagFactory
|
|
||||||
from documents.tests.factories import UserFactory
|
|
||||||
from paperless_ai.matching import extract_unmatched_names
|
from paperless_ai.matching import extract_unmatched_names
|
||||||
from paperless_ai.matching import match_correspondents_by_name
|
from paperless_ai.matching import match_correspondents_by_name
|
||||||
from paperless_ai.matching import match_document_types_by_name
|
from paperless_ai.matching import match_document_types_by_name
|
||||||
@@ -24,6 +19,11 @@ from paperless_ai.matching import resolve_correspondent_ids
|
|||||||
from paperless_ai.matching import resolve_document_type_ids
|
from paperless_ai.matching import resolve_document_type_ids
|
||||||
from paperless_ai.matching import resolve_storage_path_ids
|
from paperless_ai.matching import resolve_storage_path_ids
|
||||||
from paperless_ai.matching import resolve_tag_ids
|
from paperless_ai.matching import resolve_tag_ids
|
||||||
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
|
from paperless_testing.factories import DocumentTypeFactory
|
||||||
|
from paperless_testing.factories import StoragePathFactory
|
||||||
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
class TestAIMatching(TestCase):
|
class TestAIMatching(TestCase):
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_mock
|
import pytest_mock
|
||||||
|
|
||||||
from documents.tests.factories import CorrespondentFactory
|
|
||||||
from documents.tests.factories import DocumentFactory
|
|
||||||
from documents.tests.factories import DocumentTypeFactory
|
|
||||||
from documents.tests.factories import StoragePathFactory
|
|
||||||
from documents.tests.factories import TagFactory
|
|
||||||
from documents.tests.factories import UserFactory
|
|
||||||
from paperless_ai.taxonomy import SimilarDocument
|
from paperless_ai.taxonomy import SimilarDocument
|
||||||
from paperless_ai.taxonomy import TaxonomyCandidates
|
from paperless_ai.taxonomy import TaxonomyCandidates
|
||||||
from paperless_ai.taxonomy import build_taxonomy_candidates
|
from paperless_ai.taxonomy import build_taxonomy_candidates
|
||||||
from paperless_ai.taxonomy import format_taxonomy_for_prompt
|
from paperless_ai.taxonomy import format_taxonomy_for_prompt
|
||||||
|
from paperless_testing.factories import CorrespondentFactory
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
|
from paperless_testing.factories import DocumentTypeFactory
|
||||||
|
from paperless_testing.factories import StoragePathFactory
|
||||||
|
from paperless_testing.factories import TagFactory
|
||||||
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
def make_similar(document_id: int, weight: float) -> SimilarDocument:
|
def make_similar(document_id: int, weight: float) -> SimilarDocument:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user