Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee19e8dcff | ||
|
|
b9cf877029 | ||
|
|
82cc1016ad | ||
|
|
dcedb531c6 | ||
|
|
8bedb07cea | ||
|
|
5e34d566a2 | ||
|
|
601ecce3f1 | ||
|
|
d67beba9f6 | ||
|
|
3bc8ee8425 | ||
|
|
fdef4a99a7 | ||
|
|
d16d05a391 | ||
|
|
10789e63cb | ||
|
|
2197781b39 | ||
|
|
981492bb33 | ||
|
|
0ef5ef5826 | ||
|
|
a6b1763149 | ||
|
|
90531525e2 | ||
|
|
8f00bfa931 | ||
|
|
a0479f1d9b | ||
|
|
9b8bd21044 | ||
|
|
a60172bc6f | ||
|
|
6c5bc1c0ff | ||
|
|
f4a7c478a9 | ||
|
|
bc07c19d9b | ||
|
|
bfcee24572 | ||
|
|
4fec4b0948 | ||
|
|
3e4ffc4132 | ||
|
|
6d61bcee7e | ||
|
|
d78754bff1 | ||
|
|
5c5b1ee6b5 | ||
|
|
08f2f4bfe2 | ||
|
|
f993462973 | ||
|
|
ae70b8d60f | ||
|
|
38db6b51db | ||
|
|
31e9f4272c | ||
|
|
b8659c1af3 | ||
|
|
741115b36b | ||
|
|
1211db5cbb | ||
|
|
ca98dffbd2 | ||
|
|
4db1451e41 | ||
|
|
624b7911e5 | ||
|
|
5a1a5333ad | ||
|
|
bfe8213b78 | ||
|
|
85e192f935 | ||
|
|
4fcd4961bb | ||
|
|
5d9401ac4a | ||
|
|
6935defe7c | ||
|
|
440049978b | ||
|
|
06e9c1c02b | ||
|
|
40d09ef309 |
No files matched your search
@@ -185,22 +185,16 @@ jobs:
|
|||||||
flags: frontend-node-${{ matrix.node-version }}
|
flags: frontend-node-${{ matrix.node-version }}
|
||||||
directory: src-ui/coverage/
|
directory: src-ui/coverage/
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: E2E Tests
|
||||||
needs: [changes, install-dependencies]
|
needs: [changes, install-dependencies]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
container: mcr.microsoft.com/playwright:v1.62.0-noble
|
container: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
node-version: [24.x]
|
|
||||||
shard-index: [1, 2]
|
|
||||||
shard-count: [2]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
@@ -216,6 +210,17 @@ jobs:
|
|||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||||
|
- name: Set up Python
|
||||||
|
id: setup-python
|
||||||
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
|
with:
|
||||||
|
version: '0.12.x'
|
||||||
|
enable-cache: false
|
||||||
|
python-version: ${{ steps.setup-python.outputs.python-version }}
|
||||||
- name: Cache frontend dependencies
|
- name: Cache frontend dependencies
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
@@ -225,8 +230,17 @@ jobs:
|
|||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm install --frozen-lockfile
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
|
- name: Install backend system dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install --yes --quiet --no-install-recommends libmagic1
|
||||||
|
- name: Install backend dependencies
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
|
||||||
|
# The frozen repository lockfile and its build hooks are trusted.
|
||||||
|
run: uv sync --python "${PYTHON_VERSION}" --no-dev --frozen # NOSONAR
|
||||||
- name: Run Playwright E2E tests
|
- name: Run Playwright E2E tests
|
||||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm exec playwright test
|
||||||
frontend-build:
|
frontend-build:
|
||||||
name: Frontend Build
|
name: Frontend Build
|
||||||
needs: [changes, unit-tests, e2e-tests]
|
needs: [changes, unit-tests, e2e-tests]
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def replace_with_symlinks(
|
|||||||
total_duplicates = 0
|
total_duplicates = 0
|
||||||
space_saved = 0
|
space_saved = 0
|
||||||
|
|
||||||
for file_hash, file_list in duplicate_groups.items():
|
for file_list in duplicate_groups.values():
|
||||||
# Keep the first file as the original, replace others with symlinks
|
# Keep the first file as the original, replace others with symlinks
|
||||||
original_file = file_list[0]
|
original_file = file_list[0]
|
||||||
duplicates = file_list[1:]
|
duplicates = file_list[1:]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 501 KiB After Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 559 KiB |
|
Before Width: | Height: | Size: 644 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 667 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1003 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 925 KiB After Width: | Height: | Size: 972 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 726 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 559 KiB After Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 792 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 291 KiB |
@@ -1,5 +1,62 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.1.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: fix dark mode select disabled color, ensure disabled cursor on display mode dropdown [@shamoon](https://github.com/shamoon) ([#13881](https://github.com/paperless-ngx/paperless-ngx/pull/13881))
|
||||||
|
- Fix: add disable to the drag-drop list component [@shamoon](https://github.com/shamoon) ([#13880](https://github.com/paperless-ngx/paperless-ngx/pull/13880))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Chore: update screenshots for v3+ [@shamoon](https://github.com/shamoon) ([#13883](https://github.com/paperless-ngx/paperless-ngx/pull/13883))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>2 changes</summary>
|
||||||
|
|
||||||
|
- Fix: fix dark mode select disabled color, ensure disabled cursor on display mode dropdown [@shamoon](https://github.com/shamoon) ([#13881](https://github.com/paperless-ngx/paperless-ngx/pull/13881))
|
||||||
|
- Fix: add disable to the drag-drop list component [@shamoon](https://github.com/shamoon) ([#13880](https://github.com/paperless-ngx/paperless-ngx/pull/13880))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 3.1.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: 3.1.0 llm suggestions remove existing metadata from prompt, dont drop name suggestions [@shamoon](https://github.com/shamoon) ([#13866](https://github.com/paperless-ngx/paperless-ngx/pull/13866))
|
||||||
|
- Fix: set global search earlier to avoid awaiting debounce [@shamoon](https://github.com/shamoon) ([#13865](https://github.com/paperless-ngx/paperless-ngx/pull/13865))
|
||||||
|
- Fix: responsive sidebar, centralize and make sizes saner [@shamoon](https://github.com/shamoon) ([#13863](https://github.com/paperless-ngx/paperless-ngx/pull/13863))
|
||||||
|
- Tweak/fix: show existing count for ai suggestions [@shamoon](https://github.com/shamoon) ([#13861](https://github.com/paperless-ngx/paperless-ngx/pull/13861))
|
||||||
|
- Fix: 3.1.0 llm suggestions simplify schema, fix docstrings [@shamoon](https://github.com/shamoon) ([#13850](https://github.com/paperless-ngx/paperless-ngx/pull/13850))
|
||||||
|
- Fix: ensure ui reset of suggestionsLoading when changing docs [@shamoon](https://github.com/shamoon) ([#13840](https://github.com/paperless-ngx/paperless-ngx/pull/13840))
|
||||||
|
- Fix: always pass a non-empty api key for OpenAI-like servers [@shamoon](https://github.com/shamoon) ([#13838](https://github.com/paperless-ngx/paperless-ngx/pull/13838))
|
||||||
|
- Fix: hide slim sidebar scrollbar in browsers with stupid scrollbars [@shamoon](https://github.com/shamoon) ([#13837](https://github.com/paperless-ngx/paperless-ngx/pull/13837))
|
||||||
|
- Fix: correct sharelink bundle + document link permissions display bugs [@shamoon](https://github.com/shamoon) ([#13827](https://github.com/paperless-ngx/paperless-ngx/pull/13827))
|
||||||
|
- Fix: immediately re-add doc to index after trash restore [@shamoon](https://github.com/shamoon) ([#13818](https://github.com/paperless-ngx/paperless-ngx/pull/13818))
|
||||||
|
- Fix: navbar brand anchor size + Safari position jitter [@shamoon](https://github.com/shamoon) ([#13810](https://github.com/paperless-ngx/paperless-ngx/pull/13810))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>12 changes</summary>
|
||||||
|
|
||||||
|
- Fix: 3.1.0 llm suggestions remove existing metadata from prompt, dont drop name suggestions [@shamoon](https://github.com/shamoon) ([#13866](https://github.com/paperless-ngx/paperless-ngx/pull/13866))
|
||||||
|
- Fix: set global search earlier to avoid awaiting debounce [@shamoon](https://github.com/shamoon) ([#13865](https://github.com/paperless-ngx/paperless-ngx/pull/13865))
|
||||||
|
- Fix: responsive sidebar, centralize and make sizes saner [@shamoon](https://github.com/shamoon) ([#13863](https://github.com/paperless-ngx/paperless-ngx/pull/13863))
|
||||||
|
- Tweak/fix: show existing count for ai suggestions [@shamoon](https://github.com/shamoon) ([#13861](https://github.com/paperless-ngx/paperless-ngx/pull/13861))
|
||||||
|
- Fix: 3.1.0 llm suggestions simplify schema, fix docstrings [@shamoon](https://github.com/shamoon) ([#13850](https://github.com/paperless-ngx/paperless-ngx/pull/13850))
|
||||||
|
- Fixhancement: make imap port required, better error display [@shamoon](https://github.com/shamoon) ([#13845](https://github.com/paperless-ngx/paperless-ngx/pull/13845))
|
||||||
|
- Fix: ensure ui reset of suggestionsLoading when changing docs [@shamoon](https://github.com/shamoon) ([#13840](https://github.com/paperless-ngx/paperless-ngx/pull/13840))
|
||||||
|
- Fix: always pass a non-empty api key for OpenAI-like servers [@shamoon](https://github.com/shamoon) ([#13838](https://github.com/paperless-ngx/paperless-ngx/pull/13838))
|
||||||
|
- Fix: hide slim sidebar scrollbar in browsers with stupid scrollbars [@shamoon](https://github.com/shamoon) ([#13837](https://github.com/paperless-ngx/paperless-ngx/pull/13837))
|
||||||
|
- Fix: correct sharelink bundle + document link permissions display bugs [@shamoon](https://github.com/shamoon) ([#13827](https://github.com/paperless-ngx/paperless-ngx/pull/13827))
|
||||||
|
- Fix: immediately re-add doc to index after trash restore [@shamoon](https://github.com/shamoon) ([#13818](https://github.com/paperless-ngx/paperless-ngx/pull/13818))
|
||||||
|
- Fix: navbar brand anchor size + Safari position jitter [@shamoon](https://github.com/shamoon) ([#13810](https://github.com/paperless-ngx/paperless-ngx/pull/13810))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 3.1.0
|
## paperless-ngx 3.1.0
|
||||||
|
|
||||||
### Features / Enhancements
|
### Features / Enhancements
|
||||||
|
|||||||
@@ -2088,6 +2088,12 @@ password. All of these options come from their similarly-named [Django settings]
|
|||||||
|
|
||||||
Defaults to "always".
|
Defaults to "always".
|
||||||
|
|
||||||
|
#### [`PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=<bool>`](#PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS) {#PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS}
|
||||||
|
|
||||||
|
: If set to false, Paperless blocks remote OCR endpoint URLs that resolve to non-public addresses (e.g., localhost, etc).
|
||||||
|
|
||||||
|
Defaults to True.
|
||||||
|
|
||||||
## AI {#ai}
|
## AI {#ai}
|
||||||
|
|
||||||
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
||||||
|
|||||||
@@ -224,13 +224,19 @@ respectively, can be run non-interactively with:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm ng test
|
pnpm ng test
|
||||||
pnpm playwright test
|
pnpm e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The Playwright suite starts both the Angular development server and a disposable
|
||||||
|
Paperless instance on port 8001. The instance uses SQLite, temporary data and
|
||||||
|
media directories, and deterministic sample documents; it is removed when the
|
||||||
|
test run finishes. This requires the back-end Python dependencies from the
|
||||||
|
regular development setup to be installed with `uv sync`.
|
||||||
|
|
||||||
Playwright also includes a UI which can be run with:
|
Playwright also includes a UI which can be run with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm playwright test --ui
|
pnpm e2e:ui
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building the frontend
|
### Building the frontend
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "3.1.1"
|
version = "3.1.2"
|
||||||
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"
|
||||||
@@ -47,7 +47,7 @@ dependencies = [
|
|||||||
"httpx-oauth~=0.17",
|
"httpx-oauth~=0.17",
|
||||||
"ijson>=3.5.1",
|
"ijson>=3.5.1",
|
||||||
"imap-tools~=1.14.0",
|
"imap-tools~=1.14.0",
|
||||||
"jinja2~=3.1.5",
|
"jinja2~=3.1.6",
|
||||||
"langdetect~=1.0.9",
|
"langdetect~=1.0.9",
|
||||||
"llama-index-core>=0.14.23",
|
"llama-index-core>=0.14.23",
|
||||||
"llama-index-embeddings-huggingface>=0.6.1",
|
"llama-index-embeddings-huggingface>=0.6.1",
|
||||||
@@ -186,64 +186,117 @@ line-ending = "lf"
|
|||||||
# https://docs.astral.sh/ruff/rules/
|
# https://docs.astral.sh/ruff/rules/
|
||||||
select = [ "E4", "E7", "E9", "F" ]
|
select = [ "E4", "E7", "E9", "F" ]
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
"ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async
|
||||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
"B002", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
|
||||||
"EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
|
"B003",
|
||||||
"FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
|
"B004",
|
||||||
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
"B005",
|
||||||
"G201", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
|
"B006",
|
||||||
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
"B008",
|
||||||
"ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
|
"B009",
|
||||||
"INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
|
"B010",
|
||||||
"ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
|
"B012",
|
||||||
"PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
|
"B013",
|
||||||
"PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
"B014",
|
||||||
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
"B015",
|
||||||
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
"B016",
|
||||||
"Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
|
"B017",
|
||||||
"RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
|
"B018",
|
||||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
"B019",
|
||||||
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
"B020",
|
||||||
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
|
"B021",
|
||||||
"TC", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc
|
"B022",
|
||||||
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
|
"B023",
|
||||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
"B025",
|
||||||
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
"B026",
|
||||||
|
"B029",
|
||||||
|
"B030",
|
||||||
|
"B031",
|
||||||
|
"B032",
|
||||||
|
"B033",
|
||||||
|
"B035",
|
||||||
|
"B039",
|
||||||
|
"C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
|
||||||
|
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||||
|
"D419", # https://docs.astral.sh/ruff/rules/#pydocstyle-d
|
||||||
|
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||||
|
"DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz
|
||||||
|
"EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
|
||||||
|
"FA", # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa
|
||||||
|
"FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
|
||||||
|
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
||||||
|
"FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb
|
||||||
|
"G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
|
||||||
|
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
||||||
|
"ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
|
||||||
|
"INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
|
||||||
|
"INT", # https://docs.astral.sh/ruff/rules/#flake8-gettext-int
|
||||||
|
"ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
|
||||||
|
"LOG", # https://docs.astral.sh/ruff/rules/#flake8-logging-log
|
||||||
|
"N999", # https://docs.astral.sh/ruff/rules/#pep8-naming-n
|
||||||
|
"PERF101", # https://docs.astral.sh/ruff/rules/#perflint-perf
|
||||||
|
"PERF102",
|
||||||
|
"PERF402",
|
||||||
|
"PGH005", # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh
|
||||||
|
"PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
|
||||||
|
"PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||||
|
"PLE", # https://docs.astral.sh/ruff/rules/#error-ple
|
||||||
|
"PLR0124", # https://docs.astral.sh/ruff/rules/#refactor-plr
|
||||||
|
"PLR0133",
|
||||||
|
"PLR0206",
|
||||||
|
"PLR0402",
|
||||||
|
"PLR1704",
|
||||||
|
"PLR1708",
|
||||||
|
"PLR1711",
|
||||||
|
"PLR1716",
|
||||||
|
"PLR1722",
|
||||||
|
"PLR1730",
|
||||||
|
"PLR1733",
|
||||||
|
"PLR1736",
|
||||||
|
"PLR2044",
|
||||||
|
"PLW", # https://docs.astral.sh/ruff/rules/#warning-plw
|
||||||
|
"PT010", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
|
||||||
|
"PT014",
|
||||||
|
"PT020",
|
||||||
|
"PT025",
|
||||||
|
"PT026",
|
||||||
|
"PT031",
|
||||||
|
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
||||||
|
"Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
|
||||||
|
"RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
|
||||||
|
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||||
|
"S102", # https://docs.astral.sh/ruff/rules/#flake8-bandit-s
|
||||||
|
"S110",
|
||||||
|
"S112",
|
||||||
|
"S113",
|
||||||
|
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
||||||
|
"T100", # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10
|
||||||
|
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
|
||||||
|
"TC", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc
|
||||||
|
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
|
||||||
|
"TRY002", # https://docs.astral.sh/ruff/rules/#tryceratops-try
|
||||||
|
"TRY004",
|
||||||
|
"TRY201",
|
||||||
|
"TRY203",
|
||||||
|
"TRY401",
|
||||||
|
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
||||||
|
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
||||||
|
"YTT", # https://docs.astral.sh/ruff/rules/#flake8-2020-ytt
|
||||||
]
|
]
|
||||||
ignore = [
|
ignore = [
|
||||||
"DJ001",
|
"DJ001",
|
||||||
"PLC0415",
|
"PLC0415",
|
||||||
"RUF012",
|
"RUF012",
|
||||||
"SIM105",
|
"SIM105",
|
||||||
|
"G004", # Logging statement uses f-string - good to do, but a large diff
|
||||||
]
|
]
|
||||||
# Migrations
|
# Migrations
|
||||||
per-file-ignores."*/migrations/*.py" = [
|
per-file-ignores."*/migrations/*.py" = []
|
||||||
"E501",
|
|
||||||
"SIM",
|
|
||||||
"T201",
|
|
||||||
]
|
|
||||||
# Testing
|
# Testing
|
||||||
per-file-ignores."*/tests/*.py" = [
|
per-file-ignores."*/tests/*.py" = [
|
||||||
"E501",
|
"DTZ",
|
||||||
"SIM117",
|
"SIM117",
|
||||||
]
|
]
|
||||||
per-file-ignores.".github/scripts/*.py" = [
|
|
||||||
"E501",
|
|
||||||
"INP001",
|
|
||||||
"SIM117",
|
|
||||||
]
|
|
||||||
# Docker specific
|
|
||||||
per-file-ignores."docker/rootfs/usr/local/bin/wait-for-redis.py" = [
|
|
||||||
"INP001",
|
|
||||||
"T201",
|
|
||||||
]
|
|
||||||
per-file-ignores."docker/wait-for-redis.py" = [
|
|
||||||
"INP001",
|
|
||||||
"T201",
|
|
||||||
]
|
|
||||||
per-file-ignores."src/documents/models.py" = [
|
|
||||||
"SIM115",
|
|
||||||
]
|
|
||||||
isort.force-single-line = true
|
isort.force-single-line = true
|
||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
|
|||||||
@@ -139,6 +139,17 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"e2e": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.e2e.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"localize": [
|
||||||
|
"en-US"
|
||||||
|
]
|
||||||
|
},
|
||||||
"en-US": {
|
"en-US": {
|
||||||
"localize": [
|
"localize": [
|
||||||
"en-US"
|
"en-US"
|
||||||
@@ -155,6 +166,9 @@
|
|||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "paperless-ui:build:production"
|
"buildTarget": "paperless-ui:build:production"
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"buildTarget": "paperless-ui:build:e2e"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
{
|
|
||||||
"log": {
|
|
||||||
"version": "1.2",
|
|
||||||
"creator": {
|
|
||||||
"name": "Playwright",
|
|
||||||
"version": "1.33.0"
|
|
||||||
},
|
|
||||||
"browser": {
|
|
||||||
"name": "chromium",
|
|
||||||
"version": "113.0.5672.53"
|
|
||||||
},
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:18:59.856Z",
|
|
||||||
"time": 6.025,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/ui_settings/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "953" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":true,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"change_savedview\",\"change_schedule\",\"change_failure\",\"delete_token\",\"add_mailrule\",\"view_failure\",\"view_groupresult\",\"add_note\",\"change_taskresult\",\"view_tag\",\"view_user\",\"add_tag\",\"change_processedmail\",\"change_session\",\"view_taskattributes\",\"delete_groupresult\",\"delete_correspondent\",\"delete_schedule\",\"delete_contenttype\",\"view_chordcounter\",\"view_success\",\"delete_documenttype\",\"add_tokenproxy\",\"delete_paperlesstask\",\"add_log\",\"view_mailaccount\",\"add_uisettings\",\"view_savedview\",\"view_uisettings\",\"delete_storagepath\",\"delete_frontendsettings\",\"change_paperlesstask\",\"view_taskresult\",\"delete_processedmail\",\"view_processedmail\",\"view_session\",\"delete_chordcounter\",\"view_note\",\"delete_session\",\"view_document\",\"change_mailaccount\",\"delete_taskattributes\",\"add_groupobjectpermission\",\"view_mailrule\",\"change_savedviewfilterrule\",\"change_log\",\"change_comment\",\"add_mailaccount\",\"add_frontendsettings\",\"add_userobjectpermission\",\"delete_note\",\"view_token\",\"add_failure\",\"delete_user\",\"add_success\",\"view_ormq\",\"view_tokenproxy\",\"delete_uisettings\",\"change_groupobjectpermission\",\"add_logentry\",\"add_ormq\",\"view_frontendsettings\",\"view_schedule\",\"change_taskattributes\",\"view_documenttype\",\"view_logentry\",\"change_correspondent\",\"add_groupresult\",\"delete_groupobjectpermission\",\"change_mailrule\",\"change_permission\",\"delete_log\",\"view_userobjectpermission\",\"view_correspondent\",\"delete_document\",\"change_uisettings\",\"change_storagepath\",\"change_document\",\"delete_tokenproxy\",\"change_note\",\"delete_permission\",\"change_contenttype\",\"add_token\",\"change_success\",\"delete_logentry\",\"view_savedviewfilterrule\",\"delete_task\",\"add_savedview\",\"add_paperlesstask\",\"add_task\",\"change_documenttype\",\"add_documenttype\",\"change_token\",\"view_task\",\"view_permission\",\"change_task\",\"delete_userobjectpermission\",\"change_group\",\"add_group\",\"change_tag\",\"change_chordcounter\",\"add_storagepath\",\"delete_group\",\"add_taskattributes\",\"delete_mailaccount\",\"delete_tag\",\"add_schedule\",\"delete_failure\",\"delete_mailrule\",\"add_savedviewfilterrule\",\"change_ormq\",\"change_logentry\",\"add_taskresult\",\"view_group\",\"delete_comment\",\"add_contenttype\",\"add_document\",\"change_tokenproxy\",\"delete_success\",\"add_comment\",\"delete_ormq\",\"add_processedmail\",\"view_paperlesstask\",\"delete_savedview\",\"change_user\",\"add_session\",\"view_groupobjectpermission\",\"add_user\",\"add_correspondent\",\"delete_taskresult\",\"view_contenttype\",\"view_storagepath\",\"add_permission\",\"change_userobjectpermission\",\"delete_savedviewfilterrule\",\"change_groupresult\",\"add_chordcounter\",\"view_log\",\"view_comment\",\"change_frontendsettings\"]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 6.025 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:18:59.990Z",
|
|
||||||
"time": 1.082,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/saved_views/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "851" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 1.082 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:18:59.990Z",
|
|
||||||
"time": 0.647,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/tasks/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "2" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.647 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-settings.har')
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route('**/api/status/', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
json: {
|
||||||
|
database: { status: 'OK' },
|
||||||
|
tasks: {
|
||||||
|
redis_status: 'DISABLED',
|
||||||
|
celery_status: 'DISABLED',
|
||||||
|
index_status: 'OK',
|
||||||
|
classifier_status: 'OK',
|
||||||
|
sanity_check_status: 'OK',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('should activate / deactivate save button when settings change', async ({
|
test('should activate / deactivate save button when settings change', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/settings')
|
await page.goto('/settings')
|
||||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||||
await page.getByLabel('Use system setting').click()
|
await page.getByLabel('Use system setting').click()
|
||||||
@@ -15,7 +28,6 @@ test('should activate / deactivate save button when settings change', async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should warn on unsaved changes', async ({ page }) => {
|
test('should warn on unsaved changes', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/settings')
|
await page.goto('/settings')
|
||||||
await page.getByLabel('Use system setting').click()
|
await page.getByLabel('Use system setting').click()
|
||||||
await page.getByRole('link', { name: 'Dashboard' }).click()
|
await page.getByRole('link', { name: 'Dashboard' }).click()
|
||||||
@@ -27,7 +39,6 @@ test('should warn on unsaved changes', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should apply appearance changes when set', async ({ page }) => {
|
test('should apply appearance changes when set', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/settings')
|
await page.goto('/settings')
|
||||||
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /auto/)
|
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /auto/)
|
||||||
await page.getByLabel('Use system setting').click()
|
await page.getByLabel('Use system setting').click()
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
"""Start a disposable Paperless instance for the Playwright test suite."""
|
||||||
|
|
||||||
|
# ruff: noqa: INP001, T201
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SOURCE_ROOT = REPOSITORY_ROOT / "src"
|
||||||
|
|
||||||
|
|
||||||
|
def configure_environment(instance_root: Path) -> None:
|
||||||
|
paths = {
|
||||||
|
"PAPERLESS_CONSUMPTION_DIR": instance_root / "consume",
|
||||||
|
"PAPERLESS_DATA_DIR": instance_root / "data",
|
||||||
|
"PAPERLESS_MEDIA_ROOT": instance_root / "media",
|
||||||
|
"PAPERLESS_SCRATCH_DIR": instance_root / "scratch",
|
||||||
|
}
|
||||||
|
for name, path in paths.items():
|
||||||
|
path.mkdir(parents=True)
|
||||||
|
os.environ[name] = str(path)
|
||||||
|
(paths["PAPERLESS_DATA_DIR"] / "index").mkdir()
|
||||||
|
|
||||||
|
os.environ.update(
|
||||||
|
{
|
||||||
|
"DJANGO_SETTINGS_MODULE": "paperless.settings",
|
||||||
|
"PAPERLESS_AI_ENABLED": "false",
|
||||||
|
"PAPERLESS_CHANNELS_BACKEND": "channels.layers.InMemoryChannelLayer",
|
||||||
|
"PAPERLESS_DEBUG": "true",
|
||||||
|
"PAPERLESS_SECRET_KEY": "playwright-only-not-a-real-secret",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
sys.path.insert(0, str(SOURCE_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def seed_database() -> None:
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from documents.models import Correspondent
|
||||||
|
from documents.models import Document
|
||||||
|
from documents.models import DocumentType
|
||||||
|
from documents.models import Note
|
||||||
|
from documents.models import SavedView
|
||||||
|
from documents.models import SavedViewFilterRule
|
||||||
|
from documents.models import StoragePath
|
||||||
|
from documents.models import Tag
|
||||||
|
from documents.models import UiSettings
|
||||||
|
|
||||||
|
# Fixed credentials are safe within this disposable, localhost-only instance.
|
||||||
|
admin = User.objects.create_superuser(
|
||||||
|
username="playwright",
|
||||||
|
password="playwright", # NOSONAR
|
||||||
|
)
|
||||||
|
User.objects.create_user(
|
||||||
|
username="viewer",
|
||||||
|
password="viewer", # NOSONAR
|
||||||
|
)
|
||||||
|
|
||||||
|
inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True, owner=admin)
|
||||||
|
quick_filter = Tag.objects.create(name="Another Sample Tag", owner=admin)
|
||||||
|
Tag.objects.create(name="TagWithPartial", owner=admin)
|
||||||
|
invoice = DocumentType.objects.create(name="Invoice Test", owner=admin)
|
||||||
|
correspondent_1 = Correspondent.objects.create(
|
||||||
|
name="Test Correspondent 1",
|
||||||
|
owner=admin,
|
||||||
|
)
|
||||||
|
correspondent_2 = Correspondent.objects.create(name="Correspondent 9", owner=admin)
|
||||||
|
storage_path = StoragePath.objects.create(
|
||||||
|
name="Testing 12",
|
||||||
|
path="e2e/{created_year}/{title}",
|
||||||
|
owner=admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
today = timezone.localdate()
|
||||||
|
documents = []
|
||||||
|
for number in range(1, 62):
|
||||||
|
title = f"test document {number}" if number <= 9 else f"document {number}"
|
||||||
|
content = (
|
||||||
|
f"Playwright test content for document {number}"
|
||||||
|
if number <= 32
|
||||||
|
else f"Seeded content for document {number}"
|
||||||
|
)
|
||||||
|
created = today if number == 1 else datetime.date(2021, 1, 1)
|
||||||
|
if number in (2, 3):
|
||||||
|
created = datetime.date(2022, 12, 11)
|
||||||
|
|
||||||
|
documents.append(
|
||||||
|
Document(
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
checksum=f"{number:064x}",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
filename=f"{number:07}.pdf",
|
||||||
|
original_filename=f"document-{number}.pdf",
|
||||||
|
archive_serial_number=1122 + number if number <= 6 else None,
|
||||||
|
created=created,
|
||||||
|
owner=admin,
|
||||||
|
document_type=invoice if number <= 3 else None,
|
||||||
|
correspondent=(
|
||||||
|
correspondent_1
|
||||||
|
if number <= 4
|
||||||
|
else correspondent_2
|
||||||
|
if number <= 7
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
storage_path=storage_path if number <= 8 else None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Document.objects.bulk_create(documents)
|
||||||
|
originals = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "originals"
|
||||||
|
originals.mkdir(parents=True)
|
||||||
|
thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails"
|
||||||
|
thumbnails.mkdir(parents=True)
|
||||||
|
sample_pdf = SOURCE_ROOT / "documents" / "tests" / "samples" / "simple.pdf"
|
||||||
|
for document in documents:
|
||||||
|
shutil.copyfile(sample_pdf, originals / document.filename)
|
||||||
|
shutil.copyfile(
|
||||||
|
SOURCE_ROOT / "documents" / "resources" / "document.webp",
|
||||||
|
thumbnails / f"{document.pk:07}.webp",
|
||||||
|
)
|
||||||
|
|
||||||
|
for document in documents[:8]:
|
||||||
|
document.tags.add(inbox)
|
||||||
|
documents[0].tags.add(quick_filter)
|
||||||
|
|
||||||
|
for number in range(1, 5):
|
||||||
|
Note.objects.create(
|
||||||
|
note=f"Playwright note {number}",
|
||||||
|
document=documents[0],
|
||||||
|
user=admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
inbox_view = SavedView.objects.create(
|
||||||
|
name="Inbox",
|
||||||
|
owner=admin,
|
||||||
|
sort_field="created",
|
||||||
|
sort_reverse=True,
|
||||||
|
page_size=10,
|
||||||
|
display_mode=SavedView.DisplayMode.TABLE,
|
||||||
|
display_fields=["created", "title", "tag", "documenttype"],
|
||||||
|
)
|
||||||
|
SavedViewFilterRule.objects.create(
|
||||||
|
saved_view=inbox_view,
|
||||||
|
rule_type=6,
|
||||||
|
value=str(inbox.pk),
|
||||||
|
)
|
||||||
|
|
||||||
|
UiSettings.objects.create(
|
||||||
|
user=admin,
|
||||||
|
settings={
|
||||||
|
"language": "",
|
||||||
|
"bulk_edit": {"confirmation_dialogs": True, "apply_on_close": False},
|
||||||
|
"documentListSize": 50,
|
||||||
|
"dark_mode": {
|
||||||
|
"use_system": True,
|
||||||
|
"enabled": False,
|
||||||
|
"thumb_inverted": True,
|
||||||
|
},
|
||||||
|
"theme": {"color": "#9fbf2f"},
|
||||||
|
"document_details": {"native_pdf_viewer": False},
|
||||||
|
"date_display": {"date_locale": "", "date_format": "mediumDate"},
|
||||||
|
"comments_enabled": True,
|
||||||
|
"slim_sidebar": False,
|
||||||
|
"update_checking": {"enabled": False},
|
||||||
|
"saved_views": {
|
||||||
|
"warn_on_unsaved_change": True,
|
||||||
|
"dashboard_views_visible_ids": [inbox_view.pk],
|
||||||
|
"sidebar_views_visible_ids": [inbox_view.pk],
|
||||||
|
},
|
||||||
|
"notes_enabled": True,
|
||||||
|
"tour_complete": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
call_command(
|
||||||
|
"document_index",
|
||||||
|
"reindex",
|
||||||
|
recreate=True,
|
||||||
|
heap_size_mb=16,
|
||||||
|
verbosity=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
started = time.monotonic()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="paperless-playwright-") as instance:
|
||||||
|
configure_environment(Path(instance))
|
||||||
|
|
||||||
|
os.chdir(SOURCE_ROOT)
|
||||||
|
print("Loading the Paperless backend...", flush=True)
|
||||||
|
import django
|
||||||
|
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
settings.CELERY_TASK_ALWAYS_EAGER = True
|
||||||
|
settings.CELERY_TASK_EAGER_PROPAGATES = True
|
||||||
|
|
||||||
|
print("Migrating the disposable database...", flush=True)
|
||||||
|
call_command("migrate", interactive=False, verbosity=0)
|
||||||
|
print("Seeding Playwright data...", flush=True)
|
||||||
|
seed_database()
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"Playwright backend ready in {elapsed:.1f}s", flush=True)
|
||||||
|
settings.LOGGING["handlers"]["console"]["level"] = "WARNING"
|
||||||
|
settings.LOGGING["handlers"]["playwright_null"] = {
|
||||||
|
"class": "logging.NullHandler",
|
||||||
|
}
|
||||||
|
settings.LOGGING["loggers"]["django.server"] = {
|
||||||
|
"handlers": ["playwright_null"],
|
||||||
|
"propagate": False,
|
||||||
|
}
|
||||||
|
logging.config.dictConfig(settings.LOGGING)
|
||||||
|
call_command(
|
||||||
|
"runserver",
|
||||||
|
"localhost:8001",
|
||||||
|
use_reloader=False,
|
||||||
|
verbosity=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-dashboard1.har')
|
|
||||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-dashboard2.har')
|
|
||||||
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-dashboard3.har')
|
|
||||||
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-dashboard4.har')
|
|
||||||
|
|
||||||
test('dashboard inbox link', async ({ page }) => {
|
test('dashboard inbox link', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.getByRole('link', { name: 'Documents in inbox' }).click()
|
await page.getByRole('link', { name: 'Documents in inbox' }).click()
|
||||||
await expect(page).toHaveURL(/tags__id__in=9/)
|
await expect(page).toHaveURL(/tags__id__in=1/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dashboard total documents link', async ({ page }) => {
|
test('dashboard total documents link', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.getByRole('link').filter({ hasText: 'Total documents' }).click()
|
await page.getByRole('link').filter({ hasText: 'Total documents' }).click()
|
||||||
await expect(page).toHaveURL(/documents/)
|
await expect(page).toHaveURL(/documents/)
|
||||||
@@ -24,7 +16,6 @@ test('dashboard total documents link', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('dashboard saved view show all', async ({ page }) => {
|
test('dashboard saved view show all', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page
|
await page
|
||||||
.locator('pngx-widget-frame')
|
.locator('pngx-widget-frame')
|
||||||
@@ -32,12 +23,11 @@ test('dashboard saved view show all', async ({ page }) => {
|
|||||||
.getByRole('link', { name: 'Show all' })
|
.getByRole('link', { name: 'Show all' })
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
await expect(page).toHaveURL(/view\/7/)
|
await expect(page).toHaveURL(/view\/1/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dashboard saved view document links', async ({ page }) => {
|
test('dashboard saved view document links', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR4, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page
|
await page
|
||||||
.locator('pngx-widget-frame')
|
.locator('pngx-widget-frame')
|
||||||
@@ -46,11 +36,10 @@ test('dashboard saved view document links', async ({ page }) => {
|
|||||||
.getByRole('link', { name: /test/ })
|
.getByRole('link', { name: /test/ })
|
||||||
.first()
|
.first()
|
||||||
.click({ position: { x: 0, y: 0 } })
|
.click({ position: { x: 0, y: 0 } })
|
||||||
await expect(page).toHaveURL(/documents\/310\/details/)
|
await expect(page).toHaveURL(/documents\/1\/details/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('test slim sidebar', async ({ page }) => {
|
test('test slim sidebar', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.locator('.sidebar-slim-toggler').click()
|
await page.locator('.sidebar-slim-toggler').click()
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -1,790 +0,0 @@
|
|||||||
{
|
|
||||||
"log": {
|
|
||||||
"version": "1.2",
|
|
||||||
"creator": {
|
|
||||||
"name": "Playwright",
|
|
||||||
"version": "1.33.0"
|
|
||||||
},
|
|
||||||
"browser": {
|
|
||||||
"name": "chromium",
|
|
||||||
"version": "113.0.5672.53"
|
|
||||||
},
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.096Z",
|
|
||||||
"time": 5.747,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/ui_settings/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "948" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 5.747 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.214Z",
|
|
||||||
"time": 1.416,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/saved_views/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "851" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 1.416 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.214Z",
|
|
||||||
"time": 0.551,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/tasks/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "2" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.551 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.215Z",
|
|
||||||
"time": 1.242,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/statistics/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "257" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tags\":[9],\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 1.242 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.259Z",
|
|
||||||
"time": 0.752,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true&tags__id__all=9",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ordering",
|
|
||||||
"value": "-created"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "truncate_content",
|
|
||||||
"value": "true"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tags__id__all",
|
|
||||||
"value": "9"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "3340" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"count\":8,\"next\":null,\"previous\":null,\"all\":[310,297,298,296,290,277,312,295],\"results\":[{\"id\":310,\"correspondent\":3,\"document_type\":1,\"storage_path\":null,\"title\":\"[paperless] test post-owner\",\"content\":\"Medical Teacher\\r\\n\\r\\nISSN: 0142-159X (Print) 1466-187X (Online) Journal homepage: http://www.tandfonline.com/loi/imte20\\r\\n\\r\\nHas the new Kirkpatrick generation built a better\\r\\nhammer for our evaluation toolbox?\\r\\n\\r\\nKatherine A. Moreau\\r\\n\\r\\nTo cite this article: Katherine A. Moreau (2017) Has the new Kirkpatrick generation built\\r\\na better hammer for our evaluation toolbox?, Medical Teacher, 39:9, 999-1001, DOI:\\r\\n10.1080/0142159X.2017.1337874\\r\\n\\r\\nTo link to this article: https://doi.org/10.1080/0142159X.2017.1337874\\r\\n\\r\\nPublished online: 26 Jun 2017.\\r\\n\\r\\nS\",\"tags\":[9,2,20],\"created\":\"2023-03-26T00:00:00Z\",\"created_date\":\"2023-03-26\",\"modified\":\"2023-04-27T21:13:54.604887Z\",\"added\":\"2022-12-14T06:20:26.891909Z\",\"archive_serial_number\":null,\"original_file_name\":\"2023-03-26 Corresp Owned by Test [paperless] test post-owner.pdf\",\"archived_file_name\":\"2023-03-26 Corresp Owned by Test [paperless] test post-owner.pdf\",\"owner\":15,\"user_can_change\":false,\"notes\":[]},{\"id\":297,\"correspondent\":12,\"document_type\":null,\"storage_path\":null,\"title\":\"1 Testing New Title Updated 2\",\"content\":\"one,1.0\\nfive,5.0\",\"tags\":[4,9,5],\"created\":\"2022-10-03T00:00:00Z\",\"created_date\":\"2022-10-03\",\"modified\":\"2023-03-18T03:22:11.771968Z\",\"added\":\"2022-10-03T00:53:12.731161Z\",\"archive_serial_number\":null,\"original_file_name\":\"2022-10-03 Correspondent 9 1 Testing New Title Updated 2.txt\",\"archived_file_name\":null,\"owner\":2,\"user_can_change\":true,\"notes\":[{\"id\":65,\"note\":\"hiya\",\"created\":\"2023-04-29T07:10:13.732931Z\",\"document\":297,\"user\":2}]},{\"id\":298,\"correspondent\":3,\"document_type\":null,\"storage_path\":null,\"title\":\"Sample100.csv\",\"content\":\"Serial Number,Company Name,Employee Markme,Description,Leave\\r\\n9788189999599,TALES OF SHIVA,Mark,mark,0\\r\\n9780099578079,1Q84 THE COMPLETE TRILOGY,HARUKI MURAKAMI,Mark,0\\r\\n9780198082897,MY KUMAN,Mark,Mark,0\\r\\n9780007880331,THE GOD OF SMAAL THINGS,ARUNDHATI ROY,4TH HARPER COLLINS,2\\r\\n9780545060455,THE BLACK CIRCLE,Mark,4TH HARPER COLLINS,0\\r\\n9788126525072,THE THREE LAWS OF PERFORMANCE,Mark,4TH HARPER COLLINS,0\\r\\n9789381626610,CHAMarkKYA MANTRA,Mark,4TH HARPER COLLINS,0\\r\\n9788184513523,59.FLAGS,Mark,4TH HARPER COLLINS,0\\r\\n9780743234801,THE POWER OF POSITIV\",\"tags\":[9,10,2],\"created\":\"2022-10-03T00:00:00Z\",\"created_date\":\"2022-10-03\",\"modified\":\"2023-05-04T06:35:51.600152Z\",\"added\":\"2022-10-03T06:54:52.615096Z\",\"archive_serial_number\":112412326,\"original_file_name\":\"2022-10-03 Corresp Owned by Test Sample100.csv.csv\",\"archived_file_name\":null,\"owner\":null,\"user_can_change\":true,\"notes\":[{\"id\":64,\"note\":\"testing\",\"created\":\"2023-04-29T06:50:02.993401Z\",\"document\":298,\"user\":2}]},{\"id\":296,\"correspondent\":1,\"document_type\":1,\"storage_path\":null,\"title\":\"UM_PPBE_en_v29\",\"content\":\"PowerPanel® Business Edition \\nUser’s Manual \\n\\nRev. 29 \\n\\n2018/04 \\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nELECTRONIC END USER LICENSE AGREEMENT FOR CYBERPOWER POWERPANEL BUSINESS \\n\\nEDITION \\n\\nNOTICE TO USER: \\n\\nTHIS IS A CONTRACT. BY INSTALLING THIS SOFTWARE YOU ACCEPT ALL THE TERMS AND \\n\\nCONDITIONS OF THIS AGREEMENT. The End User License Agreement and copyright of CyberPower \\n\\nPowerPanel® Business Edition product and related explanatory materials (\\\"Software\\\") are owned by Cyber \\n\\nPower Systems (USA), Inc. The term \\\"Software\\\" also shall include any upgrades, modified\",\"tags\":[4,9],\"created\":\"2022-10-02T00:00:00Z\",\"created_date\":\"2022-10-02\",\"modified\":\"2023-05-12T06:11:20.780276Z\",\"added\":\"2022-10-02T07:17:09.462696Z\",\"archive_serial_number\":null,\"original_file_name\":\"2022-10-02 Test Correspondent 1 UM_PPBE_en_v29.pdf\",\"archived_file_name\":\"2022-10-02 Test Correspondent 1 UM_PPBE_en_v29.pdf\",\"owner\":2,\"user_can_change\":true,\"notes\":[{\"id\":58,\"note\":\"add another\",\"created\":\"2023-04-29T05:59:04.944315Z\",\"document\":296,\"user\":2},{\"id\":59,\"note\":\"test again\",\"created\":\"2023-04-29T05:59:26.727002Z\",\"document\":296,\"user\":2},{\"id\":67,\"note\":\"This is a new note\",\"created\":\"2023-05-12T16:17:07.202260Z\",\"document\":296,\"user\":2},{\"id\":68,\"note\":\"Testing another note\",\"created\":\"2023-05-12T16:51:30.556198Z\",\"document\":296,\"user\":2}]},{\"id\":290,\"correspondent\":null,\"document_type\":null,\"storage_path\":2,\"title\":\"drylab Test\",\"content\":\"Drylab(cid:78)(cid:101)(cid:119)(cid:115)\\n\\n(cid:102)(cid:111)(cid:114)(cid:32)(cid:105)(cid:110)(cid:118)(cid:101)(cid:115)(cid:116)Line truncated
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.752 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.259Z",
|
|
||||||
"time": 0.495,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ordering",
|
|
||||||
"value": "-created"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "truncate_content",
|
|
||||||
"value": "true"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "3593" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"count\":61,\"next\":\"http://localhost:8000/api/documents/?ordering=-created&page=2&page_size=10&truncate_content=true\",\"previous\":null,\"all\":[310,307,297,298,296,290,277,268,261,260,252,243,223,284,220,204,179,175,196,177,176,7,5,181,205,206,191,227,180,3,224,4,233,312,258,295,236,254,242,278,246,255,232,234,219,250,248,249,247,237,230,238,228,251,244,231,256,225,241,235,259],\"results\":[{\"id\":310,\"correspondent\":3,\"document_type\":1,\"storage_path\":null,\"title\":\"[paperless] test post-owner\",\"content\":\"Medical Teacher\\r\\n\\r\\nISSN: 0142-159X (Print) 1466-187X (Online) Journal homepage: http://www.tandfonline.com/loi/imte20\\r\\n\\r\\nHas the new Kirkpatrick generation built a better\\r\\nhammer for our evaluation toolbox?\\r\\n\\r\\nKatherine A. Moreau\\r\\n\\r\\nTo cite this article: Katherine A. Moreau (2017) Has the new Kirkpatrick generation built\\r\\na better hammer for our evaluation toolbox?, Medical Teacher, 39:9, 999-1001, DOI:\\r\\n10.1080/0142159X.2017.1337874\\r\\n\\r\\nTo link to this article: https://doi.org/10.1080/0142159X.2017.1337874\\r\\n\\r\\nPublished online: 26 Jun 2017.\\r\\n\\r\\nS\",\"tags\":[9,2,20],\"created\":\"2023-03-26T00:00:00Z\",\"created_date\":\"2023-03-26\",\"modified\":\"2023-04-27T21:13:54.604887Z\",\"added\":\"2022-12-14T06:20:26.891909Z\",\"archive_serial_number\":null,\"original_file_name\":\"2023-03-26 Corresp Owned by Test [paperless] test post-owner.pdf\",\"archived_file_name\":\"2023-03-26 Corresp Owned by Test [paperless] test post-owner.pdf\",\"owner\":15,\"user_can_change\":false,\"notes\":[]},{\"id\":307,\"correspondent\":null,\"document_type\":null,\"storage_path\":null,\"title\":\"tablerates\",\"content\":\"Country,Region/State,\\\"Zip/Postal Code\\\",\\\"Weight (and above)\\\",\\\"Shipping Price\\\"\",\"tags\":[5],\"created\":\"2022-12-12T00:00:00Z\",\"created_date\":\"2022-12-12\",\"modified\":\"2023-03-18T20:45:15.922135Z\",\"added\":\"2022-12-12T21:23:06.607087Z\",\"archive_serial_number\":null,\"original_file_name\":\"2022-12-12 tablerates.txt\",\"archived_file_name\":null,\"owner\":null,\"user_can_change\":true,\"notes\":[{\"id\":34,\"note\":\"Hiya\",\"created\":\"2023-03-18T20:45:15.231332Z\",\"document\":307,\"user\":2}]},{\"id\":297,\"correspondent\":12,\"document_type\":null,\"storage_path\":null,\"title\":\"1 Testing New Title Updated 2\",\"content\":\"one,1.0\\nfive,5.0\",\"tags\":[4,9,5],\"created\":\"2022-10-03T00:00:00Z\",\"created_date\":\"2022-10-03\",\"modified\":\"2023-03-18T03:22:11.771968Z\",\"added\":\"2022-10-03T00:53:12.731161Z\",\"archive_serial_number\":null,\"original_file_name\":\"2022-10-03 Correspondent 9 1 Testing New Title Updated 2.txt\",\"archived_file_name\":null,\"owner\":2,\"user_can_change\":true,\"notes\":[{\"id\":65,\"note\":\"hiya\",\"created\":\"2023-04-29T07:10:13.732931Z\",\"document\":297,\"user\":2}]},{\"id\":298,\"correspondent\":3,\"document_type\":null,\"storage_path\":null,\"title\":\"Sample100.csv\",\"content\":\"Serial Number,Company Name,Employee Markme,Description,Leave\\r\\n9788189999599,TALES OF SHIVA,Mark,mark,0\\r\\n9780099578079,1Q84 THE COMPLETE TRILOGY,HARUKI MURAKAMI,Mark,0\\r\\n9780198082897,MY KUMAN,Mark,Mark,0\\r\\n9780007880331,THE GOD OF SMAAL THINGS,ARUNDHATI ROY,4TH HARPER COLLINS,2\\r\\n9780545060455,THE BLACK CIRCLE,Mark,4TH HARPER COLLINS,0\\r\\n9788126525072,THE THREE LAWS OF PERFORMANCE,Mark,4TH HARPER COLLINS,0\\r\\n9789381626610,CHAMarkKYA MANTRA,Mark,4TH HARPER COLLINS,0\\r\\n9788184513523,59.FLAGS,Mark,4TH HARPER COLLINS,0\\r\\n9780743234801,THE POWER OF POSITIV\",\"tags\":[9,10,2],\"created\":\"2022-10-03T00:00:00Z\",\"created_date\":\"2022-10-03\",\"modified\":\"2023-05-04T06:35:51.600152Z\",\"added\":\"2022-10-03T06:54:52.615096Z\",\"archive_serial_number\":112412326,\"original_file_name\":\"2022-10-03 Corresp Owned by Test Sample100.csv.csv\",\"archived_file_name\":null,\"owner\":null,\"user_can_change\":true,\"notes\":[{\"id\":64,\"note\":\"testing\",\"created\":\"2023-04-29T06:50:02.993401Z\",\"document\":298,\"user\":2}]},{\"id\":296,\"correspondent\":1,\"document_type\":1,\"storage_path\":null,\"title\":\"UM_PPBE_en_v29\",\"content\":\"PowerPanel® Business Edition \\nUser’s Manual \\n\\nRev. 29 \\n\\n2018/04 \\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nELECTRONIC END USER LICENSE AGREEMENT FOR CYBERPOWER POWERPANEL BUSINESS \\n\\nEDITION \\n\\nNOTICE TO USER: \\n\\nTHIS IS A CONTRACT. BY INSTALLING THIS SOFTWARE YOU ACCEPT ALL THE TERMS AND \\n\\nCONDITIONS OF THIS AGREEMENT. The End User License Agreement and copyright of CyberPower \\n\\nPowerPanel® Business Edition product and related explanatory materials (\\\"Software\\\") are owned by Cyber \\n\\nPower Systems (USA), Inc. The term \\\"Software\\\" also shall include any upgrades, modified\",\"tags\":[4,9],\"created\":\"2022-10-02T00:00:00Z\",\"created_date\":\"2022-10-02\",\"modified\":\"2023-05-12T06:11:20.780276Z\",\"added\":\"2022-10-02T07:17:0Line truncated
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.495 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.318Z",
|
|
||||||
"time": -1,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/tags/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": -1,
|
|
||||||
"statusText": "",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "x-unknown"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": "",
|
|
||||||
"_failureText": "net::ERR_ABORTED"
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": -1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.378Z",
|
|
||||||
"time": -1,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/correspondents/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": -1,
|
|
||||||
"statusText": "",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "x-unknown"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": -1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.378Z",
|
|
||||||
"time": 0.4,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/document_types/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "388" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"count\":2,\"next\":null,\"previous\":null,\"all\":[2,1],\"results\":[{\"id\":2,\"slug\":\"another-type\",\"name\":\"Another Type\",\"match\":\"\",\"matching_algorithm\":6,\"is_insensitive\":true,\"document_count\":1,\"owner\":2,\"user_can_change\":true},{\"id\":1,\"slug\":\"invoice-test\",\"name\":\"Invoice Test\",\"match\":\"\",\"matching_algorithm\":1,\"is_insensitive\":true,\"document_count\":9,\"owner\":null,\"user_can_change\":true}]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.4 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.378Z",
|
|
||||||
"time": 0.491,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/storage_paths/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "244" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"count\":3,\"next\":null,\"previous\":null,\"all\":[5,8,2],\"results\":[{\"id\":5,\"slug\":\"testing-12\",\"name\":\"Testing 12\",\"path\":\"{created_year}-{title}\",\"match\":\"\",\"matching_algorithm\":1,\"is_insensitive\":true,\"document_count\":8,\"owner\":null,\"user_can_change\":true},{\"id\":8,\"slug\":\"testpath2\",\"name\":\"TestPath2\",\"path\":\"subdir2/{created_year}-{title}\",\"match\":\"\",\"matching_algorithm\":6,\"is_insensitive\":true,\"document_count\":3,\"owner\":null,\"user_can_change\":true},{\"id\":2,\"slug\":\"year-title\",\"name\":\"Year - Title\",\"path\":\"subdir_new_edited/{created_year}/{title}\",\"match\":\"\",\"matching_algorithm\":6,\"is_insensitive\":true,\"document_count\":4,\"owner\":null,\"user_can_change\":true}]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.491 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.378Z",
|
|
||||||
"time": -1,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/users/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": -1,
|
|
||||||
"statusText": "",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "x-unknown"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": -1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.378Z",
|
|
||||||
"time": -1,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/documents/310/?full_perms=true",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "full_perms",
|
|
||||||
"value": "true"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": -1,
|
|
||||||
"statusText": "",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "x-unknown"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": -1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.379Z",
|
|
||||||
"time": -1,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/tags/?page=1&page_size=100000",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page_size",
|
|
||||||
"value": "100000"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": -1,
|
|
||||||
"statusText": "",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "x-unknown"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": -1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:01:03.390Z",
|
|
||||||
"time": -1,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/documents/310/?full_perms=true",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [
|
|
||||||
{
|
|
||||||
"name": "full_perms",
|
|
||||||
"value": "true"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": -1,
|
|
||||||
"statusText": "",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "x-unknown"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": -1 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
import { expect, test, type WebSocketRoute } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
|
|
||||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-detail2.har')
|
|
||||||
|
|
||||||
test('should activate / deactivate save button when changes are saved', async ({
|
test('should activate / deactivate save button when changes are saved', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.goto('/documents/1/')
|
||||||
await page.goto('/documents/175/')
|
|
||||||
await page.waitForSelector('pngx-document-detail pngx-input-text:first-child')
|
await page.waitForSelector('pngx-document-detail pngx-input-text:first-child')
|
||||||
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||||
/\w+/
|
/\w+/
|
||||||
@@ -19,8 +14,7 @@ test('should activate / deactivate save button when changes are saved', async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should warn on unsaved changes', async ({ page }) => {
|
test('should warn on unsaved changes', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.goto('/documents/1/')
|
||||||
await page.goto('/documents/175/')
|
|
||||||
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
|
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
|
||||||
/\w+/
|
/\w+/
|
||||||
)
|
)
|
||||||
@@ -38,28 +32,27 @@ test('should warn on unsaved changes', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should support tab direct navigation', async ({ page }) => {
|
test('should support tab direct navigation', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.goto('/documents/1/details')
|
||||||
await page.goto('/documents/175/details')
|
|
||||||
await expect(page.getByRole('tab', { name: 'Details' })).toHaveAttribute(
|
await expect(page.getByRole('tab', { name: 'Details' })).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
)
|
)
|
||||||
await page.goto('/documents/175/content')
|
await page.goto('/documents/1/content')
|
||||||
await expect(page.getByRole('tab', { name: 'Content' })).toHaveAttribute(
|
await expect(page.getByRole('tab', { name: 'Content' })).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
)
|
)
|
||||||
await page.goto('/documents/175/metadata')
|
await page.goto('/documents/1/metadata')
|
||||||
await expect(page.getByRole('tab', { name: 'Metadata' })).toHaveAttribute(
|
await expect(page.getByRole('tab', { name: 'Metadata' })).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
)
|
)
|
||||||
await page.goto('/documents/175/notes')
|
await page.goto('/documents/1/notes')
|
||||||
await expect(page.getByRole('tab', { name: 'Notes' })).toHaveAttribute(
|
await expect(page.getByRole('tab', { name: 'Notes' })).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
)
|
)
|
||||||
await page.goto('/documents/175/permissions')
|
await page.goto('/documents/1/permissions')
|
||||||
await expect(page.getByRole('tab', { name: 'Permissions' })).toHaveAttribute(
|
await expect(page.getByRole('tab', { name: 'Permissions' })).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
@@ -67,8 +60,7 @@ test('should support tab direct navigation', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should show a mobile preview', async ({ page }) => {
|
test('should show a mobile preview', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.goto('/documents/1/')
|
||||||
await page.goto('/documents/175/')
|
|
||||||
await page.setViewportSize({ width: 400, height: 1000 })
|
await page.setViewportSize({ width: 400, height: 1000 })
|
||||||
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
||||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||||
@@ -76,8 +68,7 @@ test('should show a mobile preview', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should show a list of notes', async ({ page }) => {
|
test('should show a list of notes', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.goto('/documents/1/notes')
|
||||||
await page.goto('/documents/175/notes')
|
|
||||||
await expect(page.locator('pngx-document-notes')).toBeVisible()
|
await expect(page.locator('pngx-document-notes')).toBeVisible()
|
||||||
await expect(
|
await expect(
|
||||||
await page.getByRole('button', {
|
await page.getByRole('button', {
|
||||||
@@ -88,32 +79,25 @@ test('should show a list of notes', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should support quick filters', async ({ page }) => {
|
test('should support quick filters', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
await page.goto('/documents/1/details')
|
||||||
await page.goto('/documents/175/details')
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'Filter documents with these Tags' })
|
.getByRole('button', { name: 'Filter documents with these Tags' })
|
||||||
.click()
|
.click()
|
||||||
await expect(page).toHaveURL(/tags__id__all=4&sort=created&reverse=1&page=1/)
|
await expect(page).toHaveURL(
|
||||||
|
/tags__id__all=2,1&sort=created&reverse=1&page=1/
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should finish reloading the preview after a remote document update', async ({
|
test('should finish reloading the preview after a remote document update', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
let resolveStatusSocket: (socket: WebSocketRoute) => void
|
|
||||||
const statusSocketReady = new Promise<WebSocketRoute>((resolve) => {
|
|
||||||
resolveStatusSocket = resolve
|
|
||||||
})
|
|
||||||
await page.routeWebSocket(/\/ws\/status\/$/, (socket) => {
|
|
||||||
resolveStatusSocket(socket)
|
|
||||||
})
|
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
let previewRequestCount = 0
|
let previewRequestCount = 0
|
||||||
page.on('request', (request) => {
|
page.on('request', (request) => {
|
||||||
if (request.url().includes('/api/documents/175/preview/')) {
|
if (request.url().includes('/api/documents/1/preview/')) {
|
||||||
previewRequestCount++
|
previewRequestCount++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await page.goto('/documents/175/details')
|
await page.goto('/documents/1/details')
|
||||||
|
|
||||||
await page.locator('pngx-document-detail').waitFor()
|
await page.locator('pngx-document-detail').waitFor()
|
||||||
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||||
@@ -128,27 +112,37 @@ test('should finish reloading the preview after a remote document update', async
|
|||||||
expect(previewWasLoaded).toBe(true)
|
expect(previewWasLoaded).toBe(true)
|
||||||
const previewRequestsBeforeReload = previewRequestCount
|
const previewRequestsBeforeReload = previewRequestCount
|
||||||
|
|
||||||
const statusSocket = await statusSocketReady
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const detail = document.querySelector('pngx-document-detail')
|
||||||
|
return (window as any).ng.getComponent(detail).networkActive()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.toBe(false)
|
||||||
const documentReloaded = page.waitForResponse(
|
const documentReloaded = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('/api/documents/175/?full_perms=true') &&
|
response.url().includes('/api/documents/1/?full_perms=true') &&
|
||||||
response.request().method() === 'GET'
|
response.request().method() === 'GET'
|
||||||
)
|
)
|
||||||
statusSocket.send(
|
await page.evaluate(() => {
|
||||||
JSON.stringify({
|
const detail = document.querySelector('pngx-document-detail')
|
||||||
type: 'document_updated',
|
const component = (window as any).ng.getComponent(detail)
|
||||||
data: {
|
component.handleIncomingDocumentUpdated({
|
||||||
document_id: 175,
|
document_id: 1,
|
||||||
modified: '2026-07-26T20:00:00Z',
|
modified: '2099-07-26T20:00:00Z',
|
||||||
},
|
|
||||||
})
|
})
|
||||||
)
|
})
|
||||||
await documentReloaded
|
await documentReloaded
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByText('Document reloaded with latest changes.').first()
|
|
||||||
).toBeVisible()
|
|
||||||
await expect
|
await expect
|
||||||
.poll(() => previewRequestCount)
|
.poll(() => previewRequestCount)
|
||||||
.toBeGreaterThan(previewRequestsBeforeReload + 1)
|
.toBeGreaterThan(previewRequestsBeforeReload)
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const detail = document.querySelector('pngx-document-detail')
|
||||||
|
return (window as any).ng.getComponent(detail).previewLoaded()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
})
|
})
|
||||||
@@ -1,19 +1,10 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-document-list1.har')
|
|
||||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-list2.har')
|
|
||||||
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-document-list3.har')
|
|
||||||
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-document-list4.har')
|
|
||||||
const REQUESTS_HAR5 = path.join(__dirname, 'requests/api-document-list5.har')
|
|
||||||
const REQUESTS_HAR6 = path.join(__dirname, 'requests/api-document-list6.har')
|
|
||||||
|
|
||||||
test('basic filtering', async ({ page }) => {
|
test('basic filtering', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.getByRole('button', { name: 'Tags' }).click()
|
await page.getByRole('button', { name: 'Tags' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Inbox' }).click()
|
await page.getByRole('menuitem', { name: 'Inbox' }).click()
|
||||||
await expect(page).toHaveURL(/tags__id__all=9/)
|
await expect(page).toHaveURL(/tags__id__all=1/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||||
await page.getByRole('button', { name: 'Document type' }).click()
|
await page.getByRole('button', { name: 'Document type' }).click()
|
||||||
await page.getByRole('menuitem', { name: /^Invoice Test/ }).click()
|
await page.getByRole('menuitem', { name: /^Invoice Test/ }).click()
|
||||||
@@ -23,28 +14,27 @@ test('basic filtering', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Correspondent' }).click()
|
await page.getByRole('button', { name: 'Correspondent' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Test Correspondent 1' }).click()
|
await page.getByRole('menuitem', { name: 'Test Correspondent 1' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Correspondent 9' }).click()
|
await page.getByRole('menuitem', { name: 'Correspondent 9' }).click()
|
||||||
await expect(page).toHaveURL(/correspondent__id__in=12,1/)
|
await expect(page).toHaveURL(/correspondent__id__in=(?:1,2|2,1)/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/7 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/7 documents/)
|
||||||
await page
|
await page
|
||||||
.locator('pngx-filter-editor')
|
.locator('pngx-filter-editor')
|
||||||
.getByTitle('Correspondent')
|
.getByTitle('Correspondent')
|
||||||
.getByText('Exclude')
|
.getByText('Exclude')
|
||||||
.click()
|
.click()
|
||||||
await expect(page).toHaveURL(/correspondent__id__none=12,1/)
|
await expect(page).toHaveURL(/correspondent__id__none=(?:1,2|2,1)/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/54 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/54 documents/)
|
||||||
// clear button
|
// clear button
|
||||||
await page.getByRole('button', { name: '2 selected', exact: true }).click()
|
await page.getByRole('button', { name: '2 selected', exact: true }).click()
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/61 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/61 documents/)
|
||||||
await page.getByRole('button', { name: 'Storage path' }).click()
|
await page.getByRole('button', { name: 'Storage path' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Testing 12' }).click()
|
await page.getByRole('menuitem', { name: 'Testing 12' }).click()
|
||||||
await expect(page).toHaveURL(/storage_path__id__in=5/)
|
await expect(page).toHaveURL(/storage_path__id__in=1/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||||
await page.getByRole('button', { name: 'Reset filters' }).first().click()
|
await page.getByRole('button', { name: 'Reset filters' }).first().click()
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/61 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/61 documents/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('text filtering', async ({ page }) => {
|
test('text filtering', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.getByRole('main').getByRole('combobox').click()
|
await page.getByRole('main').getByRole('combobox').click()
|
||||||
await page.getByRole('main').getByRole('combobox').fill('test')
|
await page.getByRole('main').getByRole('combobox').fill('test')
|
||||||
@@ -57,7 +47,7 @@ test('text filtering', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Title', exact: true }).click()
|
await page.getByRole('button', { name: 'Title', exact: true }).click()
|
||||||
await page.getByRole('button', { name: 'Advanced search' }).click()
|
await page.getByRole('button', { name: 'Advanced search' }).click()
|
||||||
await expect(page).toHaveURL(/query=test/)
|
await expect(page).toHaveURL(/query=test/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/26 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/32 documents/)
|
||||||
await page.getByRole('button', { name: 'Advanced search' }).click()
|
await page.getByRole('button', { name: 'Advanced search' }).click()
|
||||||
await page.getByRole('button', { name: 'ASN' }).click()
|
await page.getByRole('button', { name: 'ASN' }).click()
|
||||||
await page.getByRole('main').getByRole('combobox').nth(1).fill('1123')
|
await page.getByRole('main').getByRole('combobox').nth(1).fill('1123')
|
||||||
@@ -80,7 +70,6 @@ test('text filtering', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('date filtering', async ({ page }) => {
|
test('date filtering', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.getByRole('button', { name: 'Dates' }).click()
|
await page.getByRole('button', { name: 'Dates' }).click()
|
||||||
await page.locator('.ng-arrow-wrapper').first().click()
|
await page.locator('.ng-arrow-wrapper').first().click()
|
||||||
@@ -94,15 +83,14 @@ test('date filtering', async ({ page }) => {
|
|||||||
await page.getByRole('option', { name: 'Within 3 months' }).click()
|
await page.getByRole('option', { name: 'Within 3 months' }).click()
|
||||||
await page.getByLabel('Dates selected').locator('button').first().click()
|
await page.getByLabel('Dates selected').locator('button').first().click()
|
||||||
await page.getByLabel('Dates selected').locator('button').first().click()
|
await page.getByLabel('Dates selected').locator('button').first().click()
|
||||||
await page.getByRole('combobox', { name: 'Select month' }).selectOption('12')
|
const createdFrom = page.getByRole('textbox', { name: 'mm/dd/yyyy' }).first()
|
||||||
await page.getByRole('combobox', { name: 'Select year' }).selectOption('2022')
|
await createdFrom.fill('12/11/2022')
|
||||||
await page.getByText('11', { exact: true }).click()
|
await createdFrom.press('Enter')
|
||||||
await page.getByRole('button', { name: 'Title & content' }).click()
|
await page.getByRole('button', { name: 'Title & content' }).click()
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/2 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/3 documents/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sorting', async ({ page }) => {
|
test('sorting', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR4, { notFound: 'fallback' })
|
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.getByRole('button', { name: 'Sort' }).click()
|
await page.getByRole('button', { name: 'Sort' }).click()
|
||||||
await page.getByRole('button', { name: 'ASN' }).click()
|
await page.getByRole('button', { name: 'ASN' }).click()
|
||||||
@@ -140,7 +128,6 @@ test('sorting', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('change views', async ({ page }) => {
|
test('change views', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.locator('.btn-group > label').first().click()
|
await page.locator('.btn-group > label').first().click()
|
||||||
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
||||||
@@ -151,12 +138,18 @@ test('change views', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('bulk edit', async ({ page }) => {
|
test('bulk edit', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR6, { notFound: 'fallback' })
|
const uiSettingsLoaded = page.waitForResponse(
|
||||||
|
(response) => response.url().includes('/api/ui_settings/') && response.ok()
|
||||||
|
)
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
|
await uiSettingsLoaded
|
||||||
|
|
||||||
await page.locator('pngx-document-card-small').nth(0).click()
|
|
||||||
await page
|
await page
|
||||||
.locator('pngx-document-card-small')
|
.locator('pngx-document-card-small .doc-img-container')
|
||||||
|
.nth(0)
|
||||||
|
.click()
|
||||||
|
await page
|
||||||
|
.locator('pngx-document-card-small .doc-img-container')
|
||||||
.nth(3)
|
.nth(3)
|
||||||
.click({
|
.click({
|
||||||
modifiers: ['Shift'],
|
modifiers: ['Shift'],
|
||||||
@@ -176,8 +169,14 @@ test('bulk edit', async ({ page }) => {
|
|||||||
)
|
)
|
||||||
await page.getByRole('button', { name: 'None' }).click()
|
await page.getByRole('button', { name: 'None' }).click()
|
||||||
|
|
||||||
await page.locator('pngx-document-card-small').nth(1).click()
|
await page
|
||||||
await page.locator('pngx-document-card-small').nth(2).click()
|
.locator('pngx-document-card-small .doc-img-container')
|
||||||
|
.nth(1)
|
||||||
|
.click()
|
||||||
|
await page
|
||||||
|
.locator('pngx-document-card-small .doc-img-container')
|
||||||
|
.nth(2)
|
||||||
|
.click()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tags' }).click()
|
await page.getByRole('button', { name: 'Tags' }).click()
|
||||||
await page
|
await page
|
||||||
@@ -185,15 +184,26 @@ test('bulk edit', async ({ page }) => {
|
|||||||
.fill('TagWithPartial')
|
.fill('TagWithPartial')
|
||||||
await page.getByRole('menuitem', { name: 'TagWithPartial' }).click()
|
await page.getByRole('menuitem', { name: 'TagWithPartial' }).click()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Apply' }).click()
|
|
||||||
|
|
||||||
const bulkEditPromise = page.waitForRequest((request) => {
|
const bulkEditPromise = page.waitForRequest((request) => {
|
||||||
|
if (
|
||||||
|
!request.url().includes('/api/documents/bulk_edit/') ||
|
||||||
|
request.method() !== 'POST'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
const postData = request.postDataJSON()
|
const postData = request.postDataJSON()
|
||||||
let isValid = postData['method'] == 'modify_tags'
|
let isValid = postData['method'] == 'modify_tags'
|
||||||
isValid = isValid && postData['parameters']['add_tags'].includes(5)
|
isValid =
|
||||||
return request.url().toString().includes('bulk_edit') && isValid
|
isValid &&
|
||||||
|
[
|
||||||
|
...postData['parameters']['add_tags'],
|
||||||
|
...postData['parameters']['remove_tags'],
|
||||||
|
].includes(3)
|
||||||
|
return isValid
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Apply' }).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Confirm' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click()
|
await page.getByRole('button', { name: 'Confirm' }).click()
|
||||||
await bulkEditPromise
|
await bulkEditPromise
|
||||||
})
|
})
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-global-permissions.har')
|
test.use({
|
||||||
|
extraHTTPHeaders: {
|
||||||
|
Authorization: `Basic ${Buffer.from('viewer:viewer').toString('base64')}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
test('should not allow user to edit settings', async ({ page }) => {
|
test('should not allow user to edit settings', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
|
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
|
||||||
await page.goto('/settings')
|
await page.goto('/settings')
|
||||||
@@ -14,7 +16,6 @@ test('should not allow user to edit settings', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view documents', async ({ page }) => {
|
test('should not allow user to view documents', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('nav').getByRole('link', { name: 'Documents' })
|
page.locator('nav').getByRole('link', { name: 'Documents' })
|
||||||
@@ -30,7 +31,6 @@ test('should not allow user to view documents', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view correspondents', async ({ page }) => {
|
test('should not allow user to view correspondents', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
@@ -42,7 +42,6 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view tags', async ({ page }) => {
|
test('should not allow user to view tags', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
@@ -54,7 +53,6 @@ test('should not allow user to view tags', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view document types', async ({ page }) => {
|
test('should not allow user to view document types', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
@@ -66,7 +64,6 @@ test('should not allow user to view document types', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view storage paths', async ({ page }) => {
|
test('should not allow user to view storage paths', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
@@ -78,7 +75,6 @@ test('should not allow user to view storage paths', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view logs', async ({ page }) => {
|
test('should not allow user to view logs', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
|
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
|
||||||
await page.goto('/logs')
|
await page.goto('/logs')
|
||||||
@@ -88,7 +84,6 @@ test('should not allow user to view logs', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should not allow user to view tasks', async ({ page }) => {
|
test('should not allow user to view tasks', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
|
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
|
||||||
await page.goto('/tasks')
|
await page.goto('/tasks')
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
{
|
|
||||||
"log": {
|
|
||||||
"version": "1.2",
|
|
||||||
"creator": {
|
|
||||||
"name": "Playwright",
|
|
||||||
"version": "1.33.0"
|
|
||||||
},
|
|
||||||
"browser": {
|
|
||||||
"name": "chromium",
|
|
||||||
"version": "113.0.5672.53"
|
|
||||||
},
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:16:51.455Z",
|
|
||||||
"time": 5.787,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/ui_settings/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "385" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 5.787 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:16:51.578Z",
|
|
||||||
"time": 0.566,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/tasks/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "2" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.566 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:16:51.578Z",
|
|
||||||
"time": 0.452,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/statistics/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "257" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.452 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:16:51.691Z",
|
|
||||||
"time": 0.891,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/ui_settings/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Encoding", "value": "br" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "385" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[]}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.891 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:16:51.739Z",
|
|
||||||
"time": 0.405,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/tasks/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "2" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.405 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"startedDateTime": "2023-05-14T07:16:51.739Z",
|
|
||||||
"time": 0.665,
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/api/statistics/",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Accept", "value": "application/json; version=3" },
|
|
||||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
|
||||||
{ "name": "Accept-Language", "value": "en-US" },
|
|
||||||
{ "name": "Connection", "value": "keep-alive" },
|
|
||||||
{ "name": "Host", "value": "localhost:8000" },
|
|
||||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
|
||||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
|
||||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
|
||||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
|
||||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
|
||||||
],
|
|
||||||
"queryString": [],
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"status": 200,
|
|
||||||
"statusText": "OK",
|
|
||||||
"httpVersion": "HTTP/1.1",
|
|
||||||
"cookies": [],
|
|
||||||
"headers": [
|
|
||||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
|
||||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
|
||||||
{ "name": "Content-Language", "value": "en-us" },
|
|
||||||
{ "name": "Content-Length", "value": "257" },
|
|
||||||
{ "name": "Content-Type", "value": "application/json" },
|
|
||||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
|
||||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
|
||||||
{ "name": "X-Api-Version", "value": "3" },
|
|
||||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
|
||||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
|
||||||
{ "name": "X-Version", "value": "1.14.4" }
|
|
||||||
],
|
|
||||||
"content": {
|
|
||||||
"size": -1,
|
|
||||||
"mimeType": "application/json",
|
|
||||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
|
||||||
},
|
|
||||||
"headersSize": -1,
|
|
||||||
"bodySize": -1,
|
|
||||||
"redirectURL": ""
|
|
||||||
},
|
|
||||||
"cache": {},
|
|
||||||
"timings": { "send": -1, "wait": -1, "receive": 0.665 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "3.1.1",
|
"version": "3.1.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
|
"e2e": "playwright test",
|
||||||
|
"e2e:backend": "uv run --project .. --no-sync python e2e/backend.py",
|
||||||
|
"e2e:ui": "playwright test --ui",
|
||||||
|
"start:e2e": "ng serve --configuration=e2e",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint"
|
"lint": "ng lint"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,17 +14,25 @@ export default defineConfig({
|
|||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 3 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Keep parallelism modest for the shared SQLite backend. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: 2,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run the disposable backend and local UI before starting the tests. */
|
||||||
webServer: {
|
webServer: [
|
||||||
port,
|
{
|
||||||
command: 'pnpm run start',
|
url: 'http://localhost:8001/accounts/login/',
|
||||||
reuseExistingServer: !process.env.CI,
|
command: 'npm run e2e:backend',
|
||||||
timeout: 2 * 60 * 1000,
|
reuseExistingServer: false,
|
||||||
},
|
timeout: 2 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port,
|
||||||
|
command: 'npm run start:e2e',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 2 * 60 * 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews() || !canSaveSettings"
|
<li class="nav-item app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews() || !canSaveSettings"
|
||||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||||
(cdkDragEnded)="onDragEnd($event)">
|
(cdkDragEnded)="onDragEnd($event)">
|
||||||
<a class="nav-link" routerLink="view/{{view.id}}"
|
<a class="nav-link" routerLink="view/{{view.id}}"
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
||||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
<div class="position-absolute end-0 top-0 px-1 py-2" [class.me-n2]="slimSidebarEnabled" cdkDragHandle>
|
||||||
<i-bs name="grip-vertical"></i-bs>
|
<i-bs name="grip-vertical"></i-bs>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
<div class="text-muted small d-flex align-items-center flex-wrap nav-label">
|
<div class="text-muted small d-flex align-items-center flex-wrap nav-label">
|
||||||
<div class="me-3">
|
<div class="me-2">
|
||||||
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
|
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
|
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
|
||||||
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@if (!settingsService.updateCheckingIsSet || appRemoteVersion()) {
|
@if (!settingsService.updateCheckingIsSet || appRemoteVersion()) {
|
||||||
<div class="version-check">
|
<div class="version-check d-flex align-items-center">
|
||||||
<ng-template #updateAvailablePopContent>
|
<ng-template #updateAvailablePopContent>
|
||||||
<span class="small">Paperless-ngx {{ appRemoteVersion().version }} <ng-container i18n>is
|
<span class="small">Paperless-ngx {{ appRemoteVersion().version }} <ng-container i18n>is
|
||||||
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
|
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@if (useDropdown) {
|
@if (useDropdown) {
|
||||||
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
|
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled" [aria-label]="title">
|
<button class="btn btn-sm" [ngClass]="!editing && isActive ? 'btn-primary' : 'btn-outline-primary'" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled" [aria-label]="title">
|
||||||
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
|
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
|
||||||
@if (isActive) {
|
@if (isActive) {
|
||||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
getLocaleNumberSymbol,
|
getLocaleNumberSymbol,
|
||||||
|
NgClass,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
NumberSymbol,
|
NumberSymbol,
|
||||||
} from '@angular/common'
|
} from '@angular/common'
|
||||||
@@ -48,25 +49,26 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
|
|||||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||||
|
|
||||||
export class CustomFieldQueriesModel {
|
export class CustomFieldQueriesModel {
|
||||||
private _queries: CustomFieldQueryElement[] = []
|
private readonly _queries = signal<CustomFieldQueryElement[]>([])
|
||||||
private rootSubscriptions: Subscription[] = []
|
private rootSubscriptions: Subscription[] = []
|
||||||
|
|
||||||
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
||||||
|
|
||||||
public get queries(): CustomFieldQueryElement[] {
|
public get queries(): CustomFieldQueryElement[] {
|
||||||
return this._queries
|
return this._queries()
|
||||||
}
|
}
|
||||||
|
|
||||||
public set queries(value: CustomFieldQueryElement[]) {
|
public set queries(value: CustomFieldQueryElement[]) {
|
||||||
this.teardownRootSubscriptions()
|
this.teardownRootSubscriptions()
|
||||||
this._queries = value ?? []
|
const queries = value ?? []
|
||||||
for (const element of this._queries) {
|
for (const element of queries) {
|
||||||
this.rootSubscriptions.push(
|
this.rootSubscriptions.push(
|
||||||
element.changed.subscribe(() => {
|
element.changed.subscribe(() => {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
this._queries.set(queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(fireEvent = true) {
|
public clear(fireEvent = true) {
|
||||||
@@ -209,6 +211,7 @@ export class CustomFieldQueriesModel {
|
|||||||
DocumentLinkComponent,
|
DocumentLinkComponent,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbDatepickerModule,
|
NgbDatepickerModule,
|
||||||
|
NgClass,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<div class="d-flex flex-wrap flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
<div class="d-flex flex-wrap flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||||
cdkDropList #selectedList="cdkDropList"
|
cdkDropList #selectedList="cdkDropList"
|
||||||
cdkDropListOrientation="mixed"
|
cdkDropListOrientation="mixed"
|
||||||
|
[cdkDropListDisabled]="disabled"
|
||||||
(cdkDropListDropped)="drop($event)"
|
(cdkDropListDropped)="drop($event)"
|
||||||
[cdkDropListConnectedTo]="[unselectedList]">
|
[cdkDropListConnectedTo]="[unselectedList]">
|
||||||
@for (item of selectedItems; track item.id) {
|
@for (item of selectedItems; track item.id) {
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<div class="d-flex flex-wrap flex-row gap-2 w-100" style="min-height: 1em;"
|
<div class="d-flex flex-wrap flex-row gap-2 w-100" style="min-height: 1em;"
|
||||||
cdkDropList #unselectedList="cdkDropList"
|
cdkDropList #unselectedList="cdkDropList"
|
||||||
cdkDropListOrientation="mixed"
|
cdkDropListOrientation="mixed"
|
||||||
|
[cdkDropListDisabled]="disabled"
|
||||||
(cdkDropListDropped)="drop($event)"
|
(cdkDropListDropped)="drop($event)"
|
||||||
[cdkDropListConnectedTo]="[selectedList]">
|
[cdkDropListConnectedTo]="[selectedList]">
|
||||||
@for (item of unselectedItems; track item.id) {
|
@for (item of unselectedItems; track item.id) {
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
.badge {
|
.badge {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cdk-drop-list-disabled {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
|
||||||
|
* {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,4 +98,29 @@ describe('DragDropSelectComponent', () => {
|
|||||||
{ id: '3', name: 'Item 3' },
|
{ id: '3', name: 'Item 3' },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should disable drag and drop when the control is disabled', () => {
|
||||||
|
component.items = [
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
]
|
||||||
|
component.writeValue(['1', '2'])
|
||||||
|
component.setDisabledState(true)
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.selectedList.disabled).toBe(true)
|
||||||
|
expect(component.unselectedList.disabled).toBe(true)
|
||||||
|
|
||||||
|
component.drop({
|
||||||
|
previousContainer: component.selectedList,
|
||||||
|
container: component.selectedList,
|
||||||
|
previousIndex: 0,
|
||||||
|
currentIndex: 1,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(component.selectedItems).toEqual([
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -46,6 +46,8 @@ export class DragDropSelectComponent extends AbstractInputComponent<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public drop(event: CdkDragDrop<string[]>) {
|
public drop(event: CdkDragDrop<string[]>) {
|
||||||
|
if (this.disabled) return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.previousContainer === event.container &&
|
event.previousContainer === event.container &&
|
||||||
event.container === this.selectedList
|
event.container === this.selectedList
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-none d-sm-flex flex-fill me-3">
|
<div class="d-none d-sm-flex flex-fill me-3">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<span class="input-group-text border-0" i18n>Select:</span>
|
<span class="input-group-text bg-transparent border-0" i18n>Select:</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm flex-nowrap">
|
<div class="btn-group btn-group-sm flex-nowrap">
|
||||||
@if (list.hasSelection) {
|
@if (list.hasSelection) {
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body rounded shadow-sm">
|
||||||
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules()" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules()" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||||
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1034,6 +1034,49 @@ describe('FilterEditorComponent', () => {
|
|||||||
).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
|
).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should reflect ingested custom field query rules in the dropdown toggle', () => {
|
||||||
|
const dropdown = fixture.debugElement.query(
|
||||||
|
By.css('pngx-custom-fields-query-dropdown')
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
||||||
|
).toBeNull()
|
||||||
|
|
||||||
|
// switching to a view with a custom field query
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_CUSTOM_FIELDS_QUERY,
|
||||||
|
value: '["OR",[[42,"exists","true"]]]',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
||||||
|
).not.toBeNull()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement
|
||||||
|
.querySelector('#dropdown_toggle')
|
||||||
|
.classList.contains('btn-primary')
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
|
// and back to a view without one
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_TAGS_ALL,
|
||||||
|
value: '19',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
||||||
|
).toBeNull()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement
|
||||||
|
.querySelector('#dropdown_toggle')
|
||||||
|
.classList.contains('btn-primary')
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner', () => {
|
it('should ingest filter rules for owner', () => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
|
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
|
||||||
<select class="form-select" formControlName="display_mode">
|
<select class="form-select form-control" formControlName="display_mode">
|
||||||
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
|
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
|
||||||
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
|
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
|
||||||
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
|
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export const DEFAULT_APP_TITLE = 'Paperless-ngx'
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiBaseUrl: 'http://localhost:8001/api/',
|
||||||
|
apiVersion: '10',
|
||||||
|
appTitle: DEFAULT_APP_TITLE,
|
||||||
|
tag: 'e2e',
|
||||||
|
version: 'E2E',
|
||||||
|
webSocketHost: 'localhost:8001',
|
||||||
|
webSocketProtocol: 'ws:',
|
||||||
|
webSocketBaseUrl: '/ws/',
|
||||||
|
}
|
||||||
@@ -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.1.1',
|
version: '3.1.2',
|
||||||
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/',
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ $grid-breakpoints: (
|
|||||||
);
|
);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--bs-border-radius: #{$border-radius};
|
||||||
|
|
||||||
@each $name, $value in $grid-breakpoints {
|
@each $name, $value in $grid-breakpoints {
|
||||||
--bs-breakpoint-#{$name}: #{$value};
|
--bs-breakpoint-#{$name}: #{$value};
|
||||||
}
|
}
|
||||||
@@ -78,19 +80,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
--bs-btn-border-radius: .425rem;
|
--bs-border-radius-sm: #{$border-radius};
|
||||||
--bs-border-radius-sm: .425rem;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control,
|
|
||||||
.form-select,
|
|
||||||
.input-group-text {
|
|
||||||
border-radius: .425rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination, .input-group {
|
.pagination, .input-group {
|
||||||
--bs-border-radius-sm: .425rem;
|
--bs-border-radius-sm: #{$border-radius};
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(min-width: 768px) {
|
@media(min-width: 768px) {
|
||||||
@@ -689,10 +684,6 @@ table.table {
|
|||||||
--bs-toast-max-width: var(--pngx-toast-max-width);
|
--bs-toast-max-width: var(--pngx-toast-max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
|
||||||
--bs-border-radius: .425rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-primary {
|
.alert-primary {
|
||||||
--bs-alert-color: var(--bs-primary);
|
--bs-alert-color: var(--bs-primary);
|
||||||
--bs-alert-bg: var(--pngx-primary-faded);
|
--bs-alert-bg: var(--pngx-primary-faded);
|
||||||
@@ -824,8 +815,6 @@ code {
|
|||||||
--bs-accordion-bg: var(--bs-light);
|
--bs-accordion-bg: var(--bs-light);
|
||||||
--bs-accordion-active-color: var(--bs-primary);
|
--bs-accordion-active-color: var(--bs-primary);
|
||||||
--bs-accordion-active-bg: var(--pngx-bg-alt);
|
--bs-accordion-active-bg: var(--pngx-bg-alt);
|
||||||
--bs-border-radius: .425rem;
|
|
||||||
--bs-accordion-inner-border-radius: calc(.425rem - 1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-button::after {
|
.accordion-button::after {
|
||||||
@@ -849,7 +838,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Animate items as they're being sorted. */
|
/* Animate items as they're being sorted. */
|
||||||
.cdk-drop-list-dragging .cdk-drag {
|
.cdk-drop-list-dragging .cdk-drag:not(.cdk-drag-preview) {
|
||||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,%3csvg xmlns='h
|
|||||||
--bs-danger-rgb: 183, 22, 49;
|
--bs-danger-rgb: 183, 22, 49;
|
||||||
--bs-body-bg: #161618;
|
--bs-body-bg: #161618;
|
||||||
--bs-body-bg-rgb: 22, 22, 24;
|
--bs-body-bg-rgb: 22, 22, 24;
|
||||||
|
--bs-secondary-bg: var(--pngx-bg-disabled);
|
||||||
|
--bs-secondary-bg-rgb: 36, 37, 41;
|
||||||
--bs-light: #1c1c1f;
|
--bs-light: #1c1c1f;
|
||||||
--bs-light-rgb: 28, 28, 31;
|
--bs-light-rgb: 28, 28, 31;
|
||||||
--bs-info: var(--pngx-bg-alt);
|
--bs-info: var(--pngx-bg-alt);
|
||||||
@@ -111,6 +113,7 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,%3csvg xmlns='h
|
|||||||
--bs-tertiary-bg: var(--pngx-bg-darker);
|
--bs-tertiary-bg: var(--pngx-bg-darker);
|
||||||
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
||||||
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15); // slightly darker than bs default
|
||||||
|
|
||||||
.text-dark, .text-light {
|
.text-dark, .text-light {
|
||||||
color: var(--bs-body-color) !important;
|
color: var(--bs-body-color) !important;
|
||||||
|
|||||||
@@ -507,8 +507,8 @@ def rotate(
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"Queued new rotated version for document {pair.root_doc.id} by {degrees} degrees",
|
f"Queued new rotated version for document {pair.root_doc.id} by {degrees} degrees",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(f"Error rotating document {pair.root_doc.id}: {e}")
|
logger.exception(f"Error rotating document {pair.root_doc.id}")
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@@ -554,9 +554,9 @@ def merge(
|
|||||||
affected_docs.append(doc.id)
|
affected_docs.append(doc.id)
|
||||||
if handoff_asn is None and doc.archive_serial_number is not None:
|
if handoff_asn is None and doc.archive_serial_number is not None:
|
||||||
handoff_asn = doc.archive_serial_number
|
handoff_asn = doc.archive_serial_number
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Error merging document {doc.id}, it will not be included in the merge: {e}",
|
f"Error merging document {doc.id}, it will not be included in the merge",
|
||||||
)
|
)
|
||||||
if len(affected_docs) == 0:
|
if len(affected_docs) == 0:
|
||||||
logger.warning("No documents were merged")
|
logger.warning("No documents were merged")
|
||||||
@@ -805,8 +805,8 @@ def split(
|
|||||||
else:
|
else:
|
||||||
group(consume_tasks).delay()
|
group(consume_tasks).delay()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(f"Error splitting document {doc.id}: {e}")
|
logger.exception(f"Error splitting document {doc.id}")
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@@ -858,8 +858,8 @@ def delete_pages(
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"Queued new version for document {pair.root_doc.id} after deleting pages {pages}",
|
f"Queued new version for document {pair.root_doc.id} after deleting pages {pages}",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(f"Error deleting pages from document {pair.root_doc.id}: {e}")
|
logger.exception(f"Error deleting pages from document {pair.root_doc.id}")
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@@ -986,7 +986,7 @@ def edit_pdf(
|
|||||||
group(consume_tasks).delay()
|
group(consume_tasks).delay()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error editing document {pair.root_doc.id}: {e}")
|
logger.exception(f"Error editing document {pair.root_doc.id}")
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"An error occurred while editing the document: {e}",
|
f"An error occurred while editing the document: {e}",
|
||||||
) from e
|
) from e
|
||||||
@@ -1097,7 +1097,7 @@ def remove_password(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Error removing password from document {pair.root_doc.id}: {e}",
|
f"Error removing password from document {pair.root_doc.id}",
|
||||||
)
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"An error occurred while removing the password: {e}",
|
f"An error occurred while removing the password: {e}",
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ from django.core.cache import cache
|
|||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from paperless.signed_pickle import SignedPickleError
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless.signed_pickle import signed_pickle_loads
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.core.cache.backends.base import BaseCache
|
from django.core.cache.backends.base import BaseCache
|
||||||
@@ -118,9 +121,11 @@ class StoredLRUCache(LRUCache):
|
|||||||
serialized_data = self._backend.get(self._backend_key)
|
serialized_data = self._backend.get(self._backend_key)
|
||||||
try:
|
try:
|
||||||
self._data = (
|
self._data = (
|
||||||
pickle.loads(serialized_data) if serialized_data else OrderedDict()
|
signed_pickle_loads(serialized_data)
|
||||||
|
if serialized_data
|
||||||
|
else OrderedDict()
|
||||||
)
|
)
|
||||||
except pickle.PickleError:
|
except (SignedPickleError, pickle.PickleError):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cache exists in backend but could not be read (possibly invalid format)",
|
"Cache exists in backend but could not be read (possibly invalid format)",
|
||||||
)
|
)
|
||||||
@@ -132,7 +137,7 @@ class StoredLRUCache(LRUCache):
|
|||||||
"""
|
"""
|
||||||
self._backend.set(
|
self._backend.set(
|
||||||
self._backend_key,
|
self._backend_key,
|
||||||
pickle.dumps(self._data),
|
signed_pickle_dumps(self._data),
|
||||||
self.backend_ttl,
|
self.backend_ttl,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ from documents.caching import CLASSIFIER_VERSION_KEY
|
|||||||
from documents.caching import StoredLRUCache
|
from documents.caching import StoredLRUCache
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
|
from paperless.signed_pickle import SignedPickleError
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless.signed_pickle import signed_pickle_loads
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.classifier")
|
logger = logging.getLogger("paperless.classifier")
|
||||||
|
|
||||||
@@ -69,8 +72,8 @@ def load_classifier(*, raise_exception: bool = False) -> DocumentClassifier | No
|
|||||||
Path(settings.MODEL_FILE).unlink()
|
Path(settings.MODEL_FILE).unlink()
|
||||||
classifier = None
|
classifier = None
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
raise e
|
raise
|
||||||
except ClassifierModelCorruptError as e:
|
except ClassifierModelCorruptError:
|
||||||
# there's something wrong with the model file.
|
# there's something wrong with the model file.
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Unrecoverable error while loading document "
|
"Unrecoverable error while loading document "
|
||||||
@@ -79,17 +82,17 @@ def load_classifier(*, raise_exception: bool = False) -> DocumentClassifier | No
|
|||||||
Path(settings.MODEL_FILE).unlink()
|
Path(settings.MODEL_FILE).unlink()
|
||||||
classifier = None
|
classifier = None
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
raise e
|
raise
|
||||||
except OSError as e:
|
except OSError:
|
||||||
logger.exception("IO error while loading document classification model")
|
logger.exception("IO error while loading document classification model")
|
||||||
classifier = None
|
classifier = None
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
raise e
|
raise
|
||||||
except Exception as e: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
logger.exception("Unknown error while loading document classification model")
|
logger.exception("Unknown error while loading document classification model")
|
||||||
classifier = None
|
classifier = None
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
raise e
|
raise
|
||||||
|
|
||||||
return classifier
|
return classifier
|
||||||
|
|
||||||
@@ -527,10 +530,17 @@ class DocumentClassifier:
|
|||||||
serialized_result = read_cache.get(key)
|
serialized_result = read_cache.get(key)
|
||||||
if serialized_result is None:
|
if serialized_result is None:
|
||||||
result = self.data_vectorizer.transform([self.preprocess_content(content)])
|
result = self.data_vectorizer.transform([self.preprocess_content(content)])
|
||||||
read_cache.set(key, pickle.dumps(result), CACHE_5_MINUTES)
|
read_cache.set(key, signed_pickle_dumps(result), CACHE_5_MINUTES)
|
||||||
else:
|
else:
|
||||||
read_cache.touch(key, CACHE_5_MINUTES)
|
try:
|
||||||
result = pickle.loads(serialized_result)
|
result = signed_pickle_loads(serialized_result)
|
||||||
|
except SignedPickleError:
|
||||||
|
result = self.data_vectorizer.transform(
|
||||||
|
[self.preprocess_content(content)],
|
||||||
|
)
|
||||||
|
read_cache.set(key, signed_pickle_dumps(result), CACHE_5_MINUTES)
|
||||||
|
else:
|
||||||
|
read_cache.touch(key, CACHE_5_MINUTES)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def predict_correspondent(self, content: str) -> int | None:
|
def predict_correspondent(self, content: str) -> int | None:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from documents.data_models import DocumentMetadataOverrides
|
|||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_filename
|
from documents.file_handling import generate_filename
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
|
from documents.file_handling import validate_path_in_root
|
||||||
from documents.loggers import LoggingMixin
|
from documents.loggers import LoggingMixin
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
@@ -216,7 +217,7 @@ class ConsumerPluginMixin:
|
|||||||
current_progress,
|
current_progress,
|
||||||
max_progress,
|
max_progress,
|
||||||
document_id=document_id,
|
document_id=document_id,
|
||||||
owner_id=self.metadata.owner_id if self.metadata.owner_id else None,
|
owner_id=self.metadata.owner_id or None,
|
||||||
users_can_view=(self.metadata.view_users or [])
|
users_can_view=(self.metadata.view_users or [])
|
||||||
+ (self.metadata.change_users or []),
|
+ (self.metadata.change_users or []),
|
||||||
groups_can_view=(self.metadata.view_groups or [])
|
groups_can_view=(self.metadata.view_groups or [])
|
||||||
@@ -674,9 +675,7 @@ class ConsumerPlugin(
|
|||||||
document=document,
|
document=document,
|
||||||
logging_group=self.logging_group,
|
logging_group=self.logging_group,
|
||||||
classifier=classifier,
|
classifier=classifier,
|
||||||
original_file=self.unmodified_original
|
original_file=self.unmodified_original or self.working_copy,
|
||||||
if self.unmodified_original
|
|
||||||
else self.working_copy,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# After everything is in the database, copy the files into
|
# After everything is in the database, copy the files into
|
||||||
@@ -695,6 +694,10 @@ class ConsumerPlugin(
|
|||||||
use_format=False,
|
use_format=False,
|
||||||
)
|
)
|
||||||
document.filename = generated_filename
|
document.filename = generated_filename
|
||||||
|
validate_path_in_root(
|
||||||
|
document.source_path,
|
||||||
|
settings.ORIGINALS_DIR,
|
||||||
|
)
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
|
|
||||||
self._write(
|
self._write(
|
||||||
@@ -727,6 +730,10 @@ class ConsumerPlugin(
|
|||||||
use_format=False,
|
use_format=False,
|
||||||
)
|
)
|
||||||
document.archive_filename = generated_archive_filename
|
document.archive_filename = generated_archive_filename
|
||||||
|
validate_path_in_root(
|
||||||
|
document.archive_path,
|
||||||
|
settings.ARCHIVE_DIR,
|
||||||
|
)
|
||||||
create_source_path_directory(document.archive_path)
|
create_source_path_directory(document.archive_path)
|
||||||
self._write(
|
self._write(
|
||||||
archive_path,
|
archive_path,
|
||||||
@@ -849,7 +856,7 @@ class ConsumerPlugin(
|
|||||||
else:
|
else:
|
||||||
stats = Path(self.input_doc.original_file).stat()
|
stats = Path(self.input_doc.original_file).stat()
|
||||||
create_date = timezone.make_aware(
|
create_date = timezone.make_aware(
|
||||||
datetime.datetime.fromtimestamp(stats.st_mtime),
|
datetime.datetime.fromtimestamp(stats.st_mtime), # noqa: DTZ006 - make_aware() requires a naive datetime
|
||||||
)
|
)
|
||||||
self.log.debug(f"Creation date from st_mtime: {create_date}")
|
self.log.debug(f"Creation date from st_mtime: {create_date}")
|
||||||
|
|
||||||
@@ -963,7 +970,7 @@ class ConsumerPlugin(
|
|||||||
try:
|
try:
|
||||||
copy_basic_file_stats(source, target)
|
copy_basic_file_stats(source, target)
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
pass
|
self.log.debug("Unable to copy file stats from %s to %s", source, target)
|
||||||
|
|
||||||
|
|
||||||
class ConsumerPreflightPlugin(
|
class ConsumerPreflightPlugin(
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ class CollatePlugin(NoCleanupPluginMixin, NoSetupPluginMixin, ConsumeTaskPlugin)
|
|||||||
stats = staging.stat()
|
stats = staging.stat()
|
||||||
# if the file is older than the timeout, we don't consider
|
# if the file is older than the timeout, we don't consider
|
||||||
# it valid
|
# it valid
|
||||||
if (dt.datetime.now().timestamp() - stats.st_mtime) > TIMEOUT_SECONDS:
|
if (
|
||||||
|
dt.datetime.now(tz=dt.UTC).timestamp() - stats.st_mtime
|
||||||
|
) > TIMEOUT_SECONDS:
|
||||||
logger.warning("Outdated double sided staging file exists, deleting it")
|
logger.warning("Outdated double sided staging file exists, deleting it")
|
||||||
staging.unlink()
|
staging.unlink()
|
||||||
else:
|
else:
|
||||||
@@ -134,7 +136,7 @@ class CollatePlugin(NoCleanupPluginMixin, NoSetupPluginMixin, ConsumeTaskPlugin)
|
|||||||
shutil.move(pdf_file, staging)
|
shutil.move(pdf_file, staging)
|
||||||
# update access to modification time so we know if the file
|
# update access to modification time so we know if the file
|
||||||
# is outdated when another file gets uploaded
|
# is outdated when another file gets uploaded
|
||||||
timestamp = dt.datetime.now().timestamp()
|
timestamp = dt.datetime.now(tz=dt.UTC).timestamp()
|
||||||
os.utime(staging, (timestamp, timestamp))
|
os.utime(staging, (timestamp, timestamp))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Got scan with odd numbered pages of double-sided scan, moved it to %s",
|
"Got scan with odd numbered pages of double-sided scan, moved it to %s",
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from documents.templating.filepath import is_safe_relative_path
|
||||||
from documents.templating.filepath import validate_filepath_template_and_render
|
from documents.templating.filepath import validate_filepath_template_and_render
|
||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
|
|
||||||
|
logger = logging.getLogger("paperless.filehandling")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsafeFilePathError(Exception):
|
||||||
|
"""
|
||||||
|
Raised when a path generated for a document would land outside of its root.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def validate_path_in_root(path: Path, root: Path) -> None:
|
||||||
|
"""
|
||||||
|
Ensures the given absolute path is contained within root, the
|
||||||
|
equivalent guard for the later move.
|
||||||
|
"""
|
||||||
|
if not path.resolve().is_relative_to(root.resolve()):
|
||||||
|
msg = f"Refusing to write file outside of root {root}: {path}."
|
||||||
|
logger.warning(msg)
|
||||||
|
raise UnsafeFilePathError(msg)
|
||||||
|
|
||||||
|
|
||||||
def create_source_path_directory(source_path: Path) -> None:
|
def create_source_path_directory(source_path: Path) -> None:
|
||||||
source_path.parent.mkdir(parents=True, exist_ok=True)
|
source_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -121,6 +142,14 @@ def format_filename(document: Document, template_str: str) -> str | None:
|
|||||||
"none",
|
"none",
|
||||||
) # backward compatibility
|
) # backward compatibility
|
||||||
|
|
||||||
|
# Validate again after remove none
|
||||||
|
if not is_safe_relative_path(rendered_filename):
|
||||||
|
logger.warning(
|
||||||
|
"Filename became unsafe after placeholder removal, "
|
||||||
|
"falling back to default naming",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
return rendered_filename
|
return rendered_filename
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ class CustomFieldQueryParser:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check if any of the requested IDs are missing.
|
# Check if any of the requested IDs are missing.
|
||||||
missing_ids = set(value) - set(link.document_id for link in links)
|
missing_ids = set(value) - {link.document_id for link in links}
|
||||||
if missing_ids:
|
if missing_ids:
|
||||||
# The result should be an empty set in this case.
|
# The result should be an empty set in this case.
|
||||||
return Q(id__in=[])
|
return Q(id__in=[])
|
||||||
|
|||||||
@@ -631,23 +631,25 @@ class Command(BaseCommand):
|
|||||||
):
|
):
|
||||||
# Process each change
|
# Process each change
|
||||||
for change_type, path in changes:
|
for change_type, path in changes:
|
||||||
path = Path(path).resolve()
|
resolved_path = Path(path).resolve()
|
||||||
if change_type == Change.deleted:
|
if change_type == Change.deleted:
|
||||||
# Consumed (or otherwise removed); a later file
|
# Consumed (or otherwise removed); a later file
|
||||||
# reusing this name must not be skipped as
|
# reusing this name must not be skipped as
|
||||||
# already-queued.
|
# already-queued.
|
||||||
queued.discard(path)
|
queued.discard(resolved_path)
|
||||||
if not path.is_file():
|
if not resolved_path.is_file():
|
||||||
continue
|
continue
|
||||||
if path in queued:
|
if resolved_path in queued:
|
||||||
# Already queued and awaiting consumption; a stray
|
# Already queued and awaiting consumption; a stray
|
||||||
# event (NAS metadata touch, AV scan, etc.) while
|
# event (NAS metadata touch, AV scan, etc.) while
|
||||||
# the file sits on disk mid-consumption must not
|
# the file sits on disk mid-consumption must not
|
||||||
# cause it to be queued a second time (GH #13511).
|
# cause it to be queued a second time (GH #13511).
|
||||||
logger.debug(f"Ignoring event for queued file: {path}")
|
logger.debug(
|
||||||
|
f"Ignoring event for queued file: {resolved_path}",
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Event: {change_type.name} for {path}")
|
logger.debug(f"Event: {change_type.name} for {resolved_path}")
|
||||||
tracker.track(path, change_type)
|
tracker.track(resolved_path, change_type)
|
||||||
|
|
||||||
# Check for stable files
|
# Check for stable files
|
||||||
for stable_path in tracker.get_stable_files():
|
for stable_path in tracker.get_stable_files():
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger("paperless.matching")
|
logger = logging.getLogger("paperless.matching")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedWorkflowTriggerTypeError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def log_reason(
|
def log_reason(
|
||||||
matching_model: MatchingModel | WorkflowTrigger,
|
matching_model: MatchingModel | WorkflowTrigger,
|
||||||
document: Document,
|
document: Document,
|
||||||
@@ -691,7 +695,9 @@ def document_matches_workflow(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# New trigger types need to be explicitly checked above
|
# New trigger types need to be explicitly checked above
|
||||||
raise Exception(f"Trigger type {trigger_type} not yet supported")
|
raise UnsupportedWorkflowTriggerTypeError(
|
||||||
|
f"Trigger type {trigger_type} not yet supported",
|
||||||
|
)
|
||||||
|
|
||||||
if trigger_matched:
|
if trigger_matched:
|
||||||
logger.info(f"Document matched {trigger} from {workflow}")
|
logger.info(f"Document matched {trigger} from {workflow}")
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ def recompute_checksums(apps, schema_editor):
|
|||||||
if updated_fields:
|
if updated_fields:
|
||||||
batch.append(doc)
|
batch.append(doc)
|
||||||
|
|
||||||
processed += 1
|
processed += 1 # noqa: SIM113
|
||||||
|
|
||||||
if len(batch) >= _BATCH_SIZE:
|
if len(batch) >= _BATCH_SIZE:
|
||||||
Document.objects.bulk_update(batch, ["checksum", "archive_checksum"])
|
Document.objects.bulk_update(batch, ["checksum", "archive_checksum"])
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
|||||||
from documents.versioning import versions_newest_first
|
from documents.versioning import versions_newest_first
|
||||||
|
|
||||||
if hasattr(self, "effective_content"):
|
if hasattr(self, "effective_content"):
|
||||||
return getattr(self, "effective_content")
|
return self.effective_content
|
||||||
|
|
||||||
if self.root_document_id is not None or self.pk is None:
|
if self.root_document_id is not None or self.pk is None:
|
||||||
return self.content
|
return self.content
|
||||||
@@ -462,7 +462,11 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
|||||||
"""
|
"""
|
||||||
Returns a sanitized filename for the document, not including any paths.
|
Returns a sanitized filename for the document, not including any paths.
|
||||||
"""
|
"""
|
||||||
result = str(self)
|
# Root owns metadata for all versions
|
||||||
|
context_document = (
|
||||||
|
self.root_document if self.root_document_id is not None else self
|
||||||
|
)
|
||||||
|
result = str(context_document)
|
||||||
|
|
||||||
if counter:
|
if counter:
|
||||||
result += f"_{counter:02}"
|
result += f"_{counter:02}"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def get_default_file_extension(mime_type: str) -> str:
|
|||||||
return supported[mime_type]
|
return supported[mime_type]
|
||||||
|
|
||||||
ext = mimetypes.guess_extension(mime_type)
|
ext = mimetypes.guess_extension(mime_type)
|
||||||
return ext if ext else ""
|
return ext or ""
|
||||||
|
|
||||||
|
|
||||||
def is_file_ext_supported(ext: str) -> bool:
|
def is_file_ext_supported(ext: str) -> bool:
|
||||||
@@ -110,7 +110,7 @@ def run_convert(
|
|||||||
args += ["-define", "pdf:use-cropbox=true"] if use_cropbox else []
|
args += ["-define", "pdf:use-cropbox=true"] if use_cropbox else []
|
||||||
args += [str(input_file), str(output_file)]
|
args += [str(input_file), str(output_file)]
|
||||||
|
|
||||||
logger.debug("Execute: " + " ".join(args), extra={"group": logging_group})
|
logger.debug("Execute: %s", " ".join(args), extra={"group": logging_group})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
run_subprocess(args, environment, logger)
|
run_subprocess(args, environment, logger)
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ def _discover_parser_class() -> type[DateParserPluginBase]:
|
|||||||
valid_plugins.append(ep)
|
valid_plugins.append(ep)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Plugin {ep.name} does not subclass DateParser.")
|
logger.warning(f"Plugin {ep.name} does not subclass DateParser.")
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(f"Unable to load date parser plugin {ep.name}: {e}")
|
logger.exception(f"Unable to load date parser plugin {ep.name}")
|
||||||
|
|
||||||
if not valid_plugins:
|
if not valid_plugins:
|
||||||
return RegexDateParserPlugin
|
return RegexDateParserPlugin
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ class DateParserPluginBase(ABC):
|
|||||||
},
|
},
|
||||||
locales=self.config.languages,
|
locales=self.config.languages,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(f"Error while parsing date string '{date_string}': {e}")
|
logger.exception(f"Error while parsing date string '{date_string}'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _filter_date(
|
def _filter_date(
|
||||||
|
|||||||
@@ -59,11 +59,10 @@ def safe_regex_match(pattern: str, text: str, *, flags: int = 0):
|
|||||||
try:
|
try:
|
||||||
validate_regex_pattern(pattern)
|
validate_regex_pattern(pattern)
|
||||||
compiled = regex.compile(pattern, flags=flags)
|
compiled = regex.compile(pattern, flags=flags)
|
||||||
except (regex.error, ValueError) as exc:
|
except (regex.error, ValueError):
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Error while processing regular expression %s: %s",
|
"Error while processing regular expression %s",
|
||||||
textwrap.shorten(pattern, width=80, placeholder="…"),
|
textwrap.shorten(pattern, width=80, placeholder="…"),
|
||||||
exc,
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -86,11 +85,10 @@ def safe_regex_sub(pattern: str, repl: str, text: str, *, flags: int = 0) -> str
|
|||||||
try:
|
try:
|
||||||
validate_regex_pattern(pattern)
|
validate_regex_pattern(pattern)
|
||||||
compiled = regex.compile(pattern, flags=flags)
|
compiled = regex.compile(pattern, flags=flags)
|
||||||
except (regex.error, ValueError) as exc:
|
except (regex.error, ValueError):
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Error while processing regular expression %s: %s",
|
"Error while processing regular expression %s",
|
||||||
textwrap.shorten(pattern, width=80, placeholder="…"),
|
textwrap.shorten(pattern, width=80, placeholder="…"),
|
||||||
exc,
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1142,7 +1142,7 @@ def get_backend() -> TantivyBackend:
|
|||||||
Returns:
|
Returns:
|
||||||
Thread-safe singleton TantivyBackend instance
|
Thread-safe singleton TantivyBackend instance
|
||||||
"""
|
"""
|
||||||
global _backend, _backend_path
|
global _backend, _backend_path # noqa: PLW0603 - module-level singleton, no class to hold this state
|
||||||
|
|
||||||
current_path: Path = settings.INDEX_DIR
|
current_path: Path = settings.INDEX_DIR
|
||||||
|
|
||||||
@@ -1173,7 +1173,7 @@ def reset_backend() -> None:
|
|||||||
Forces creation of a new backend instance on the next get_backend() call.
|
Forces creation of a new backend instance on the next get_backend() call.
|
||||||
Used for test isolation and when switching between different index directories.
|
Used for test isolation and when switching between different index directories.
|
||||||
"""
|
"""
|
||||||
global _backend, _backend_path
|
global _backend, _backend_path # noqa: PLW0603 - module-level singleton, no class to hold this state
|
||||||
|
|
||||||
with _backend_lock:
|
with _backend_lock:
|
||||||
if _backend is not None:
|
if _backend is not None:
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ def parse_user_query(
|
|||||||
DEFAULT_SEARCH_FIELDS,
|
DEFAULT_SEARCH_FIELDS,
|
||||||
field_boosts=_FIELD_BOOSTS,
|
field_boosts=_FIELD_BOOSTS,
|
||||||
# (prefix=True, distance=1, transposition_cost_one=True) — edit-distance fuzziness
|
# (prefix=True, distance=1, transposition_cost_one=True) — edit-distance fuzziness
|
||||||
fuzzy_fields={f: (True, 1, True) for f in DEFAULT_SEARCH_FIELDS},
|
fuzzy_fields=dict.fromkeys(DEFAULT_SEARCH_FIELDS, (True, 1, True)),
|
||||||
)
|
)
|
||||||
# 0.1 boost keeps fuzzy hits ranked below exact matches (intentional)
|
# 0.1 boost keeps fuzzy hits ranked below exact matches (intentional)
|
||||||
clauses.append((tantivy.Occur.Should, tantivy.Query.boost_query(fuzzy, 0.1)))
|
clauses.append((tantivy.Occur.Should, tantivy.Query.boost_query(fuzzy, 0.1)))
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ from documents.permissions import get_document_count_filter_for_user
|
|||||||
from documents.permissions import get_groups_with_only_permission
|
from documents.permissions import get_groups_with_only_permission
|
||||||
from documents.permissions import has_perms_owner_aware
|
from documents.permissions import has_perms_owner_aware
|
||||||
from documents.permissions import permitted_document_ids
|
from documents.permissions import permitted_document_ids
|
||||||
|
from documents.permissions import restrict_queryset_to_visible
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
from documents.regex import validate_regex_pattern
|
from documents.regex import validate_regex_pattern
|
||||||
from documents.templating.filepath import validate_filepath_template_and_render
|
from documents.templating.filepath import validate_filepath_template_and_render
|
||||||
@@ -433,7 +434,7 @@ class OwnedObjectSerializer(
|
|||||||
return set()
|
return set()
|
||||||
|
|
||||||
ctype = ContentType.objects.get_for_model(first_obj)
|
ctype = ContentType.objects.get_for_model(first_obj)
|
||||||
object_pks = list(obj.pk for obj in objects)
|
object_pks = [obj.pk for obj in objects]
|
||||||
pk_type = type(first_obj.pk)
|
pk_type = type(first_obj.pk)
|
||||||
|
|
||||||
def get_pks_for_permission_type(model):
|
def get_pks_for_permission_type(model):
|
||||||
@@ -661,6 +662,8 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
.select_related("owner")
|
.select_related("owner")
|
||||||
.annotate(document_count=Count("documents", filter=filter_q))
|
.annotate(document_count=Count("documents", filter=filter_q))
|
||||||
)
|
)
|
||||||
|
user = getattr(request, "user", None) if request else self.user
|
||||||
|
children = restrict_queryset_to_visible(children, user, "view_tag")
|
||||||
|
|
||||||
view = self.context.get("view")
|
view = self.context.get("view")
|
||||||
ordering = (
|
ordering = (
|
||||||
@@ -727,7 +730,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
self.instance.clean()
|
self.instance.clean()
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.debug("Tag parent validation failed: %s", e)
|
logger.debug("Tag parent validation failed: %s", e)
|
||||||
raise e
|
raise
|
||||||
finally:
|
finally:
|
||||||
self.instance.tn_parent = original_parent
|
self.instance.tn_parent = original_parent
|
||||||
else:
|
else:
|
||||||
@@ -737,7 +740,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
temp.clean()
|
temp.clean()
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.debug("Tag parent validation failed: %s", e)
|
logger.debug("Tag parent validation failed: %s", e)
|
||||||
raise e
|
raise
|
||||||
|
|
||||||
return super().validate(attrs)
|
return super().validate(attrs)
|
||||||
|
|
||||||
@@ -1147,7 +1150,7 @@ class DocumentSerializer(
|
|||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
doc = super().to_representation(instance)
|
doc = super().to_representation(instance)
|
||||||
if "content" in self.fields and hasattr(instance, "effective_content"):
|
if "content" in self.fields and hasattr(instance, "effective_content"):
|
||||||
doc["content"] = getattr(instance, "effective_content") or ""
|
doc["content"] = instance.effective_content or ""
|
||||||
if self.truncate_content and "content" in self.fields:
|
if self.truncate_content and "content" in self.fields:
|
||||||
doc["content"] = doc.get("content")[0:550]
|
doc["content"] = doc.get("content")[0:550]
|
||||||
return doc
|
return doc
|
||||||
@@ -1857,8 +1860,8 @@ class BulkEditSerializer(
|
|||||||
if isinstance(custom_fields, dict):
|
if isinstance(custom_fields, dict):
|
||||||
try:
|
try:
|
||||||
ids = [int(i[0]) for i in custom_fields.items()]
|
ids = [int(i[0]) for i in custom_fields.items()]
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception(f"Error validating custom fields: {e}")
|
logger.exception("Error validating custom fields")
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
f"{name} must be a list of integers or a dict of id:value pairs, see the log for details",
|
f"{name} must be a list of integers or a dict of id:value pairs, see the log for details",
|
||||||
)
|
)
|
||||||
@@ -2056,13 +2059,12 @@ class BulkEditSerializer(
|
|||||||
for doc in docs:
|
for doc in docs:
|
||||||
if "-" in doc:
|
if "-" in doc:
|
||||||
pages.append(
|
pages.append(
|
||||||
[
|
list(
|
||||||
x
|
range(
|
||||||
for x in range(
|
|
||||||
int(doc.split("-")[0]),
|
int(doc.split("-")[0]),
|
||||||
int(doc.split("-")[1]) + 1,
|
int(doc.split("-")[1]) + 1,
|
||||||
)
|
),
|
||||||
],
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pages.append([int(doc)])
|
pages.append([int(doc)])
|
||||||
@@ -2923,7 +2925,7 @@ class ShareLinkBundleSerializer(OwnedObjectSerializer):
|
|||||||
return share_link_bundle
|
return share_link_bundle
|
||||||
|
|
||||||
def get_document_count(self, obj: ShareLinkBundle) -> int:
|
def get_document_count(self, obj: ShareLinkBundle) -> int:
|
||||||
return getattr(obj, "document_total") or obj.documents.count()
|
return obj.document_total or obj.documents.count()
|
||||||
|
|
||||||
|
|
||||||
class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ def update_filename_and_move_files(
|
|||||||
# so this is not the end of the world.
|
# so this is not the end of the world.
|
||||||
# B: if moving the original file failed, nothing has changed
|
# B: if moving the original file failed, nothing has changed
|
||||||
# anyway.
|
# anyway.
|
||||||
pass
|
logger.exception("Error reverting document changes")
|
||||||
|
|
||||||
# restore old values on the instance
|
# restore old values on the instance
|
||||||
instance.filename = old_filename
|
instance.filename = old_filename
|
||||||
@@ -1102,10 +1102,11 @@ def _extract_input_data(
|
|||||||
if v is None or k.startswith("_"):
|
if v is None or k.startswith("_"):
|
||||||
continue
|
continue
|
||||||
if isinstance(v, datetime.date):
|
if isinstance(v, datetime.date):
|
||||||
v = v.isoformat()
|
override_dict[k] = v.isoformat()
|
||||||
elif isinstance(v, Path):
|
elif isinstance(v, Path):
|
||||||
v = str(v)
|
override_dict[k] = str(v)
|
||||||
override_dict[k] = v
|
else:
|
||||||
|
override_dict[k] = v
|
||||||
if override_dict:
|
if override_dict:
|
||||||
data["overrides"] = override_dict
|
data["overrides"] = override_dict
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -217,9 +217,9 @@ def consume_file(
|
|||||||
overrides.filename or input_doc.original_file.name,
|
overrides.filename or input_doc.original_file.name,
|
||||||
self.request.id,
|
self.request.id,
|
||||||
) as status_mgr,
|
) as status_mgr,
|
||||||
TemporaryDirectory(dir=settings.SCRATCH_DIR) as tmp_dir,
|
TemporaryDirectory(dir=settings.SCRATCH_DIR) as tmp_dir_name,
|
||||||
):
|
):
|
||||||
tmp_dir = Path(tmp_dir)
|
tmp_dir = Path(tmp_dir_name)
|
||||||
msg = None
|
msg = None
|
||||||
for plugin_class in plugins:
|
for plugin_class in plugins:
|
||||||
plugin_name = plugin_class.NAME
|
plugin_name = plugin_class.NAME
|
||||||
@@ -261,7 +261,7 @@ def consume_file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"{plugin_name} failed: {e}")
|
logger.exception(f"{plugin_name} failed")
|
||||||
status_mgr.send_progress(
|
status_mgr.send_progress(
|
||||||
ProgressStatusOptions.FAILED,
|
ProgressStatusOptions.FAILED,
|
||||||
f"{e}",
|
f"{e}",
|
||||||
@@ -495,8 +495,8 @@ def empty_trash(doc_ids=None) -> None:
|
|||||||
content_type=ContentType.objects.get_for_model(Document),
|
content_type=ContentType.objects.get_for_model(Document),
|
||||||
object_id__in=deleted_document_ids,
|
object_id__in=deleted_document_ids,
|
||||||
).delete()
|
).delete()
|
||||||
except Exception as e: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
logger.exception(f"Error while emptying trash: {e}")
|
logger.exception("Error while emptying trash")
|
||||||
finally:
|
finally:
|
||||||
models.signals.post_delete.disconnect(
|
models.signals.post_delete.disconnect(
|
||||||
cleanup_document_deletion,
|
cleanup_document_deletion,
|
||||||
@@ -832,9 +832,8 @@ def build_share_link_bundle(bundle_id: int) -> None:
|
|||||||
logger.info("Built share link bundle %s", bundle.pk)
|
logger.info("Built share link bundle %s", bundle.pk)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to build share link bundle %s: %s",
|
"Failed to build share link bundle %s",
|
||||||
bundle_id,
|
bundle_id,
|
||||||
exc,
|
|
||||||
)
|
)
|
||||||
bundle.status = ShareLinkBundle.Status.FAILED
|
bundle.status = ShareLinkBundle.Status.FAILED
|
||||||
bundle.last_error = {
|
bundle.last_error = {
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ class PlaceholderString(str):
|
|||||||
def __ne__(self, other) -> bool:
|
def __ne__(self, other) -> bool:
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
# Equal to both "-none-" and "none", so hash to a single canonical value
|
||||||
|
return hash("-none-")
|
||||||
|
|
||||||
|
|
||||||
NO_VALUE_PLACEHOLDER = PlaceholderString("-none-")
|
NO_VALUE_PLACEHOLDER = PlaceholderString("-none-")
|
||||||
|
|
||||||
@@ -340,7 +344,7 @@ def get_custom_fields_context(
|
|||||||
return field_data
|
return field_data
|
||||||
|
|
||||||
|
|
||||||
def _is_safe_relative_path(value: str) -> bool:
|
def is_safe_relative_path(value: str) -> bool:
|
||||||
if value == "":
|
if value == "":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -398,7 +402,7 @@ def validate_filepath_template_and_render(
|
|||||||
)
|
)
|
||||||
rendered_template = template.render(context)
|
rendered_template = template.render(context)
|
||||||
|
|
||||||
if not _is_safe_relative_path(rendered_template):
|
if not is_safe_relative_path(rendered_template):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Template rendered an unsafe path (absolute or containing traversal).",
|
"Template rendered an unsafe path (absolute or containing traversal).",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ def parse_w_workflow_placeholders(
|
|||||||
|
|
||||||
# We're good!
|
# We're good!
|
||||||
return rendered_template
|
return rendered_template
|
||||||
except UndefinedError as e:
|
except UndefinedError:
|
||||||
# The undefined class logs this already for us
|
# The undefined class logs this already for us
|
||||||
raise e
|
raise
|
||||||
except TemplateSyntaxError as e:
|
except TemplateSyntaxError as e:
|
||||||
logger.warning(f"Template syntax error in title generation: {e}")
|
logger.warning(f"Template syntax error in title generation: {e}")
|
||||||
except SecurityError as e:
|
except SecurityError as e:
|
||||||
@@ -150,5 +150,5 @@ def parse_w_workflow_placeholders(
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"Invalid title format '{text}', workflow not applied: {e}",
|
f"Invalid title format '{text}', workflow not applied: {e}",
|
||||||
)
|
)
|
||||||
raise e
|
raise
|
||||||
return None
|
return None
|
||||||
@@ -296,7 +296,7 @@ class TestRegexDateParser:
|
|||||||
|
|
||||||
# simulate parse failure for malformed input
|
# simulate parse failure for malformed input
|
||||||
if "99/99/9999" in date_string or "bad date" in date_string:
|
if "99/99/9999" in date_string or "bad date" in date_string:
|
||||||
raise Exception("parse failed for malformed date")
|
raise Exception("parse failed for malformed date") # noqa: TRY002 - simulates a generic parser failure
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ class MultiprocessCommand(PaperlessCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
items = list(range(5))
|
items = list(range(5))
|
||||||
results = []
|
results = list(
|
||||||
for result in self.process_parallel(
|
self.process_parallel(
|
||||||
_double_value,
|
_double_value,
|
||||||
items,
|
items,
|
||||||
description="Processing...",
|
description="Processing...",
|
||||||
):
|
),
|
||||||
results.append(result)
|
)
|
||||||
successes = sum(1 for r in results if r.success)
|
successes = sum(1 for r in results if r.success)
|
||||||
self.stdout.write(f"Successes: {successes}")
|
self.stdout.write(f"Successes: {successes}")
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class TestWriteBatchLockRetry:
|
|||||||
)
|
)
|
||||||
mock_sleep = mocker.patch(
|
mock_sleep = mocker.patch(
|
||||||
"documents.search._backend.time.sleep",
|
"documents.search._backend.time.sleep",
|
||||||
side_effect=lambda s: sleep_values.append(s),
|
side_effect=sleep_values.append,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should not raise — 4th attempt succeeds
|
# Should not raise — 4th attempt succeeds
|
||||||
@@ -111,7 +111,7 @@ class TestWriteBatchLockRetry:
|
|||||||
sleep_values: list[float] = []
|
sleep_values: list[float] = []
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"documents.search._backend.time.sleep",
|
"documents.search._backend.time.sleep",
|
||||||
side_effect=lambda s: sleep_values.append(s),
|
side_effect=sleep_values.append,
|
||||||
)
|
)
|
||||||
for _ in range(50):
|
for _ in range(50):
|
||||||
sleep_values.clear()
|
sleep_values.clear()
|
||||||
|
|||||||
@@ -1063,3 +1063,79 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn("non-public address", str(response.data).lower())
|
self.assertIn("non-public address", str(response.data).lower())
|
||||||
|
|
||||||
|
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=False)
|
||||||
|
def test_update_remote_ocr_endpoint_blocks_internal_endpoint_when_disallowed(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Internal remote OCR endpoints are disallowed
|
||||||
|
WHEN:
|
||||||
|
- The config is updated with a remote OCR endpoint resolving internally
|
||||||
|
THEN:
|
||||||
|
- The request is rejected
|
||||||
|
"""
|
||||||
|
response = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"remote_ocr_endpoint": "http://127.0.0.1:5000",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn("non-public address", str(response.data).lower())
|
||||||
|
|
||||||
|
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=True)
|
||||||
|
def test_update_remote_ocr_endpoint_allows_internal_endpoint_by_default(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Internal remote OCR endpoints are allowed (the default)
|
||||||
|
WHEN:
|
||||||
|
- The config is updated with a remote OCR endpoint resolving internally
|
||||||
|
THEN:
|
||||||
|
- The request is accepted, preserving existing self-hosted deployments
|
||||||
|
"""
|
||||||
|
response = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"remote_ocr_endpoint": "http://127.0.0.1:5000",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["remote_ocr_endpoint"],
|
||||||
|
"http://127.0.0.1:5000",
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=False)
|
||||||
|
def test_update_remote_ocr_endpoint_empty_value_skips_validation(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Internal remote OCR endpoints are disallowed
|
||||||
|
WHEN:
|
||||||
|
- The config is updated with an empty remote OCR endpoint
|
||||||
|
THEN:
|
||||||
|
- The request is accepted; clearing the field never needs
|
||||||
|
outbound URL validation
|
||||||
|
"""
|
||||||
|
response = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"remote_ocr_endpoint": "",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["remote_ocr_endpoint"], "")
|
||||||
@@ -1003,8 +1003,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
for correspondent in response.data[field]:
|
for correspondent in response.data[field]:
|
||||||
self.assertEqual(correspondent["document_count"], 0)
|
self.assertEqual(correspondent["document_count"], 0)
|
||||||
self.assertCountEqual(
|
self.assertCountEqual(
|
||||||
map(lambda c: c["id"], response.data[field]),
|
(c["id"] for c in response.data[field]),
|
||||||
map(lambda c: c["id"], Entity.objects.values("id")),
|
(c["id"] for c in Entity.objects.values("id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_api_selection_data(self) -> None:
|
def test_api_selection_data(self) -> None:
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
- Last correspondence date is returned only if requested for list, and for detail
|
- Last correspondence date is returned only if requested for list, and for detail
|
||||||
|
- The date is scoped to documents the requesting user may view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Document.objects.create(
|
Document.objects.create(
|
||||||
@@ -145,6 +146,32 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
response.data["last_correspondence"],
|
response.data["last_correspondence"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A newer document owned by another user must not leak through the
|
||||||
|
# aggregate for a non-superuser who cannot view it
|
||||||
|
other = User.objects.create_user(username="other")
|
||||||
|
Document.objects.create(
|
||||||
|
mime_type="application/pdf",
|
||||||
|
correspondent=self.c1,
|
||||||
|
created=datetime.date(2023, 6, 1),
|
||||||
|
checksum="hidden",
|
||||||
|
owner=other,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.create_user(username="regular")
|
||||||
|
user.user_permissions.add(
|
||||||
|
Permission.objects.get(codename="view_correspondent"),
|
||||||
|
)
|
||||||
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
|
response = self.client.get("/api/correspondents/?last_correspondence=true")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
result = next(r for r in response.data["results"] if r["id"] == self.c1.id)
|
||||||
|
self.assertIn("2022-01-02", result["last_correspondence"])
|
||||||
|
|
||||||
|
response = self.client.get(f"/api/correspondents/{self.c1.id}/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("2022-01-02", response.data["last_correspondence"])
|
||||||
|
|
||||||
def test_paginated_objects_include_all_only_for_legacy_version(self) -> None:
|
def test_paginated_objects_include_all_only_for_legacy_version(self) -> None:
|
||||||
response_v10 = self.client.get("/api/correspondents/")
|
response_v10 = self.client.get("/api/correspondents/")
|
||||||
self.assertEqual(response_v10.status_code, status.HTTP_200_OK)
|
self.assertEqual(response_v10.status_code, status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ class MockOpenIDProvider:
|
|||||||
|
|
||||||
def get_brands(self):
|
def get_brands(self):
|
||||||
default_servers = [
|
default_servers = [
|
||||||
dict(id="yahoo", name="Yahoo", openid_url="http://me.yahoo.com"),
|
{"id": "yahoo", "name": "Yahoo", "openid_url": "http://me.yahoo.com"},
|
||||||
dict(id="hyves", name="Hyves", openid_url="http://hyves.nl"),
|
{"id": "hyves", "name": "Hyves", "openid_url": "http://hyves.nl"},
|
||||||
]
|
]
|
||||||
return default_servers
|
return default_servers
|
||||||
|
|
||||||
|
|||||||