mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-03-13 12:41:23 +00:00
Compare commits
21 Commits
fix-drop-s
...
fix-drop-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f84e41773 | ||
|
|
7da2a060d7 | ||
|
|
632f7113ca | ||
|
|
975ae11d35 | ||
|
|
0a82c5f367 | ||
|
|
b5da27967d | ||
|
|
bc1addb4e6 | ||
|
|
6857ae4f74 | ||
|
|
2994395e3e | ||
|
|
6ebd1ab6a1 | ||
|
|
37fc666734 | ||
|
|
c84cca9d9e | ||
|
|
ff4ce58f20 | ||
|
|
002e394ffa | ||
|
|
5df94da53a | ||
|
|
d86a57290a | ||
|
|
11f6c9af85 | ||
|
|
f889c54c52 | ||
|
|
dd8573242d | ||
|
|
86fa74c115 | ||
|
|
b7b9e83f37 |
@@ -56,6 +56,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
|
PAPERLESS_DBENGINE: postgres
|
||||||
env_file:
|
env_file:
|
||||||
- stack.env
|
- stack.env
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
|
PAPERLESS_DBENGINE: postgresql
|
||||||
PAPERLESS_TIKA_ENABLED: 1
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
|
PAPERLESS_DBENGINE: postgresql
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ services:
|
|||||||
env_file: docker-compose.env
|
env_file: docker-compose.env
|
||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
PAPERLESS_DBENGINE: sqlite
|
||||||
PAPERLESS_TIKA_ENABLED: 1
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ services:
|
|||||||
env_file: docker-compose.env
|
env_file: docker-compose.env
|
||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
PAPERLESS_DBENGINE: sqlite
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
media:
|
media:
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ cd "${PAPERLESS_SRC_DIR}"
|
|||||||
|
|
||||||
# The whole migrate, with flock, needs to run as the right user
|
# The whole migrate, with flock, needs to run as the right user
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py check --tag compatibility paperless
|
||||||
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
||||||
else
|
else
|
||||||
|
exec s6-setuidgid paperless \
|
||||||
|
s6-setlock -n "${data_dir}/migration_lock" \
|
||||||
|
python3 manage.py check --tag compatibility paperless
|
||||||
exec s6-setuidgid paperless \
|
exec s6-setuidgid paperless \
|
||||||
s6-setlock -n "${data_dir}/migration_lock" \
|
s6-setlock -n "${data_dir}/migration_lock" \
|
||||||
python3 manage.py migrate --skip-checks --no-input
|
python3 manage.py migrate --skip-checks --no-input
|
||||||
|
|||||||
@@ -437,3 +437,6 @@ Initial API version.
|
|||||||
moved from the bulk edit endpoint to their own individual endpoints. Using these methods via
|
moved from the bulk edit endpoint to their own individual endpoints. Using these methods via
|
||||||
the bulk edit endpoint is still supported for compatibility with versions < 10 until support
|
the bulk edit endpoint is still supported for compatibility with versions < 10 until support
|
||||||
for API v9 is dropped.
|
for API v9 is dropped.
|
||||||
|
- The `all` parameter of list endpoints is now deprecated and will be removed in a future version.
|
||||||
|
- The bulk edit objects endpoint now supports `all` and `filters` parameters to avoid having to send
|
||||||
|
large lists of object IDs for operations affecting many objects.
|
||||||
|
|||||||
@@ -468,7 +468,7 @@
|
|||||||
"time": 0.951,
|
"time": 0.951,
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=9",
|
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__in=9",
|
||||||
"httpVersion": "HTTP/1.1",
|
"httpVersion": "HTTP/1.1",
|
||||||
"cookies": [],
|
"cookies": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -534,7 +534,7 @@
|
|||||||
"time": 0.653,
|
"time": 0.653,
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9",
|
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=9",
|
||||||
"httpVersion": "HTTP/1.1",
|
"httpVersion": "HTTP/1.1",
|
||||||
"cookies": [],
|
"cookies": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
|
|||||||
@@ -883,7 +883,7 @@
|
|||||||
"time": 0.93,
|
"time": 0.93,
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=4",
|
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=4",
|
||||||
"httpVersion": "HTTP/1.1",
|
"httpVersion": "HTTP/1.1",
|
||||||
"cookies": [],
|
"cookies": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
@@ -961,7 +961,7 @@
|
|||||||
"time": -1,
|
"time": -1,
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=4",
|
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=4",
|
||||||
"httpVersion": "HTTP/1.1",
|
"httpVersion": "HTTP/1.1",
|
||||||
"cookies": [],
|
"cookies": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ test('basic filtering', async ({ page }) => {
|
|||||||
await expect(page).toHaveURL(/tags__id__all=9/)
|
await expect(page).toHaveURL(/tags__id__all=9/)
|
||||||
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 3' }).click()
|
await page.getByRole('menuitem', { name: /^Invoice Test/ }).click()
|
||||||
await expect(page).toHaveURL(/document_type__id__in=1/)
|
await expect(page).toHaveURL(/document_type__id__in=1/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/3 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/3 documents/)
|
||||||
await page.getByRole('button', { name: 'Reset filters' }).first().click()
|
await page.getByRole('button', { name: 'Reset filters' }).first().click()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -20,9 +20,9 @@ import { Subject, filter, takeUntil } from 'rxjs'
|
|||||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||||
import { MatchingModel } from 'src/app/data/matching-model'
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||||
|
import { SelectionDataItem } from 'src/app/data/results'
|
||||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||||
import { SelectionDataItem } from 'src/app/services/rest/document.service'
|
|
||||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DatePipe } from '@angular/common'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import {
|
import {
|
||||||
HttpTestingController,
|
HttpTestingController,
|
||||||
@@ -138,6 +139,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
|
DatePipe,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
@@ -298,7 +300,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
parameters: { add_tags: [101], remove_tags: [] },
|
parameters: { add_tags: [101], remove_tags: [] },
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -330,7 +332,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
.flush(true)
|
.flush(true)
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -421,7 +423,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
parameters: { correspondent: 101 },
|
parameters: { correspondent: 101 },
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -453,7 +455,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
.flush(true)
|
.flush(true)
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -519,7 +521,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
parameters: { document_type: 101 },
|
parameters: { document_type: 101 },
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -551,7 +553,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
.flush(true)
|
.flush(true)
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -617,7 +619,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
parameters: { storage_path: 101 },
|
parameters: { storage_path: 101 },
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -649,7 +651,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
.flush(true)
|
.flush(true)
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -715,7 +717,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
parameters: { add_custom_fields: [101], remove_custom_fields: [102] },
|
parameters: { add_custom_fields: [101], remove_custom_fields: [102] },
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -747,7 +749,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
.flush(true)
|
.flush(true)
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -856,7 +858,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
documents: [3, 4],
|
documents: [3, 4],
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -949,7 +951,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
documents: [3, 4],
|
documents: [3, 4],
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -984,7 +986,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
source_mode: 'latest_version',
|
source_mode: 'latest_version',
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -1025,7 +1027,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
metadata_document_id: 3,
|
metadata_document_id: 3,
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -1044,7 +1046,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
delete_originals: true,
|
delete_originals: true,
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -1065,7 +1067,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
archive_fallback: true,
|
archive_fallback: true,
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -1151,7 +1153,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
@@ -1458,7 +1460,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect(toastServiceShowInfoSpy).toHaveBeenCalled()
|
expect(toastServiceShowInfoSpy).toHaveBeenCalled()
|
||||||
expect(listReloadSpy).toHaveBeenCalled()
|
expect(listReloadSpy).toHaveBeenCalled()
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
) // list reload
|
) // list reload
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { first, map, Observable, Subject, switchMap, takeUntil } from 'rxjs'
|
|||||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||||
import { CustomField } from 'src/app/data/custom-field'
|
import { CustomField } from 'src/app/data/custom-field'
|
||||||
import { MatchingModel } from 'src/app/data/matching-model'
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
|
import { SelectionDataItem } from 'src/app/data/results'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
@@ -32,7 +33,6 @@ import {
|
|||||||
DocumentBulkEditMethod,
|
DocumentBulkEditMethod,
|
||||||
DocumentService,
|
DocumentService,
|
||||||
MergeDocumentsRequest,
|
MergeDocumentsRequest,
|
||||||
SelectionDataItem,
|
|
||||||
} from 'src/app/services/rest/document.service'
|
} from 'src/app/services/rest/document.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
|
import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import {
|
|||||||
FILTER_TITLE_CONTENT,
|
FILTER_TITLE_CONTENT,
|
||||||
NEGATIVE_NULL_FILTER_VALUE,
|
NEGATIVE_NULL_FILTER_VALUE,
|
||||||
} from 'src/app/data/filter-rule-type'
|
} from 'src/app/data/filter-rule-type'
|
||||||
|
import { SelectionData, SelectionDataItem } from 'src/app/data/results'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
@@ -84,11 +85,7 @@ import {
|
|||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import {
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
DocumentService,
|
|
||||||
SelectionData,
|
|
||||||
SelectionDataItem,
|
|
||||||
} from 'src/app/services/rest/document.service'
|
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
||||||
<i-bs name="text-indent-left"></i-bs><div class="d-none d-sm-inline ms-1"><ng-container i18n>Select</ng-container></div>
|
<i-bs name="text-indent-left"></i-bs><div class="d-none d-sm-inline ms-1"><ng-container i18n>Select</ng-container></div>
|
||||||
@if (activeManagementList.selectedObjects.size > 0) {
|
@if (activeManagementList.hasSelection) {
|
||||||
<pngx-clearable-badge [selected]="activeManagementList.selectedObjects.size > 0" [number]="activeManagementList.selectedObjects.size" (cleared)="activeManagementList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
<pngx-clearable-badge [selected]="activeManagementList.hasSelection" [number]="activeManagementList.selectedCount" (cleared)="activeManagementList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<span class="input-group-text border-0" i18n>Select:</span>
|
<span class="input-group-text 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 (activeManagementList.selectedObjects.size > 0) {
|
@if (activeManagementList.hasSelection) {
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="activeManagementList.selectNone()">
|
<button class="btn btn-sm btn-outline-secondary" (click)="activeManagementList.selectNone()">
|
||||||
<i-bs name="slash-circle" class="me-1"></i-bs><ng-container i18n>None</ng-container>
|
<i-bs name="slash-circle" class="me-1"></i-bs><ng-container i18n>None</ng-container>
|
||||||
</button>
|
</button>
|
||||||
@@ -40,11 +40,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeManagementList.setPermissions()"
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeManagementList.setPermissions()"
|
||||||
[disabled]="!activeManagementList.userCanBulkEdit(PermissionAction.Change) || activeManagementList.selectedObjects.size === 0">
|
[disabled]="!activeManagementList.userCanBulkEdit(PermissionAction.Change) || !activeManagementList.hasSelection">
|
||||||
<i-bs name="person-fill-lock" class="me-1"></i-bs><ng-container i18n>Permissions</ng-container>
|
<i-bs name="person-fill-lock" class="me-1"></i-bs><ng-container i18n>Permissions</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="activeManagementList.delete()"
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="activeManagementList.delete()"
|
||||||
[disabled]="!activeManagementList.userCanBulkEdit(PermissionAction.Delete) || activeManagementList.selectedObjects.size === 0">
|
[disabled]="!activeManagementList.userCanBulkEdit(PermissionAction.Delete) || !activeManagementList.hasSelection">
|
||||||
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="activeManagementList.openCreateDialog()"
|
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="activeManagementList.openCreateDialog()"
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
@if (displayCollectionSize > 0) {
|
@if (displayCollectionSize > 0) {
|
||||||
<div>
|
<div>
|
||||||
<ng-container i18n>{displayCollectionSize, plural, =1 {One {{typeName}}} other {{{displayCollectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
|
<ng-container i18n>{displayCollectionSize, plural, =1 {One {{typeName}}} other {{{displayCollectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
|
||||||
@if (selectedObjects.size > 0) {
|
@if (hasSelection) {
|
||||||
({{selectedObjects.size}} selected)
|
({{selectedCount}} selected)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ describe('ManagementListComponent', () => {
|
|||||||
: tags
|
: tags
|
||||||
return of({
|
return of({
|
||||||
count: results.length,
|
count: results.length,
|
||||||
all: results.map((o) => o.id),
|
|
||||||
results,
|
results,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -231,11 +230,11 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(reloadSpy).toHaveBeenCalled()
|
expect(reloadSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use API count for pagination and all ids for displayed total', fakeAsync(() => {
|
it('should use API count for pagination and nested ids for displayed total', fakeAsync(() => {
|
||||||
jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce(
|
jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce(
|
||||||
of({
|
of({
|
||||||
count: 1,
|
count: 1,
|
||||||
all: [1, 2, 3],
|
display_count: 3,
|
||||||
results: tags.slice(0, 1),
|
results: tags.slice(0, 1),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -315,13 +314,17 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(component.togggleAll).toBe(false)
|
expect(component.togggleAll).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('selectAll should use all IDs when collection size exists', () => {
|
it('selectAll should activate all-selection mode', () => {
|
||||||
;(component as any).allIDs = [1, 2, 3, 4]
|
;(tagService.listFiltered as jest.Mock).mockClear()
|
||||||
component.collectionSize = 4
|
component.collectionSize = tags.length
|
||||||
|
|
||||||
component.selectAll()
|
component.selectAll()
|
||||||
|
|
||||||
expect(component.selectedObjects).toEqual(new Set([1, 2, 3, 4]))
|
expect(tagService.listFiltered).not.toHaveBeenCalled()
|
||||||
|
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
||||||
|
expect((component as any).allSelectionActive).toBe(true)
|
||||||
|
expect(component.hasSelection).toBe(true)
|
||||||
|
expect(component.selectedCount).toBe(tags.length)
|
||||||
expect(component.togggleAll).toBe(true)
|
expect(component.togggleAll).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -395,6 +398,33 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(successToastSpy).toHaveBeenCalled()
|
expect(successToastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support bulk edit permissions for all filtered items', () => {
|
||||||
|
const bulkEditPermsSpy = jest
|
||||||
|
.spyOn(tagService, 'bulk_edit_objects')
|
||||||
|
.mockReturnValue(of('OK'))
|
||||||
|
component.selectAll()
|
||||||
|
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setPermissions()
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
|
||||||
|
modal.componentInstance.confirmClicked.emit({
|
||||||
|
permissions: {},
|
||||||
|
merge: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(bulkEditPermsSpy).toHaveBeenCalledWith(
|
||||||
|
[],
|
||||||
|
BulkEditObjectOperation.SetPermissions,
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
{ is_root: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should support bulk delete objects', () => {
|
it('should support bulk delete objects', () => {
|
||||||
const bulkEditSpy = jest.spyOn(tagService, 'bulk_edit_objects')
|
const bulkEditSpy = jest.spyOn(tagService, 'bulk_edit_objects')
|
||||||
component.toggleSelected(tags[0])
|
component.toggleSelected(tags[0])
|
||||||
@@ -415,7 +445,11 @@ describe('ManagementListComponent', () => {
|
|||||||
modal.componentInstance.confirmClicked.emit(null)
|
modal.componentInstance.confirmClicked.emit(null)
|
||||||
expect(bulkEditSpy).toHaveBeenCalledWith(
|
expect(bulkEditSpy).toHaveBeenCalledWith(
|
||||||
Array.from(selected),
|
Array.from(selected),
|
||||||
BulkEditObjectOperation.Delete
|
BulkEditObjectOperation.Delete,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
expect(errorToastSpy).toHaveBeenCalled()
|
expect(errorToastSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
@@ -426,6 +460,29 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(successToastSpy).toHaveBeenCalled()
|
expect(successToastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support bulk delete for all filtered items', () => {
|
||||||
|
const bulkEditSpy = jest
|
||||||
|
.spyOn(tagService, 'bulk_edit_objects')
|
||||||
|
.mockReturnValue(of('OK'))
|
||||||
|
|
||||||
|
component.selectAll()
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.delete()
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
|
||||||
|
modal.componentInstance.confirmClicked.emit(null)
|
||||||
|
expect(bulkEditSpy).toHaveBeenCalledWith(
|
||||||
|
[],
|
||||||
|
BulkEditObjectOperation.Delete,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
{ is_root: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should disallow bulk permissions or delete objects if no global perms', () => {
|
it('should disallow bulk permissions or delete objects if no global perms', () => {
|
||||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||||
expect(component.userCanBulkEdit(PermissionAction.Delete)).toBeFalsy()
|
expect(component.userCanBulkEdit(PermissionAction.Delete)).toBeFalsy()
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
|
|
||||||
public data: T[] = []
|
public data: T[] = []
|
||||||
private unfilteredData: T[] = []
|
private unfilteredData: T[] = []
|
||||||
private allIDs: number[] = []
|
private currentExtraParams: { [key: string]: any } = null
|
||||||
|
private allSelectionActive = false
|
||||||
|
|
||||||
public page = 1
|
public page = 1
|
||||||
|
|
||||||
@@ -107,6 +108,16 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
public selectedObjects: Set<number> = new Set()
|
public selectedObjects: Set<number> = new Set()
|
||||||
public togggleAll: boolean = false
|
public togggleAll: boolean = false
|
||||||
|
|
||||||
|
public get hasSelection(): boolean {
|
||||||
|
return this.selectedObjects.size > 0 || this.allSelectionActive
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selectedCount(): number {
|
||||||
|
return this.allSelectionActive
|
||||||
|
? this.displayCollectionSize
|
||||||
|
: this.selectedObjects.size
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
|
|
||||||
@@ -150,11 +161,11 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getCollectionSize(results: Results<T>): number {
|
protected getCollectionSize(results: Results<T>): number {
|
||||||
return results.all?.length ?? results.count
|
return results.count
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDisplayCollectionSize(results: Results<T>): number {
|
protected getDisplayCollectionSize(results: Results<T>): number {
|
||||||
return this.getCollectionSize(results)
|
return results.display_count ?? this.getCollectionSize(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocumentCount(object: MatchingModel): number {
|
getDocumentCount(object: MatchingModel): number {
|
||||||
@@ -171,6 +182,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
|
|
||||||
reloadData(extraParams: { [key: string]: any } = null) {
|
reloadData(extraParams: { [key: string]: any } = null) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
this.currentExtraParams = extraParams
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
this.service
|
this.service
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
@@ -189,7 +201,6 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.data = this.filterData(c.results)
|
this.data = this.filterData(c.results)
|
||||||
this.collectionSize = this.getCollectionSize(c)
|
this.collectionSize = this.getCollectionSize(c)
|
||||||
this.displayCollectionSize = this.getDisplayCollectionSize(c)
|
this.displayCollectionSize = this.getDisplayCollectionSize(c)
|
||||||
this.allIDs = c.all
|
|
||||||
}),
|
}),
|
||||||
delay(100)
|
delay(100)
|
||||||
)
|
)
|
||||||
@@ -346,7 +357,16 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
return objects.map((o) => o.id)
|
return objects.map((o) => o.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getBulkEditFilters(): { [key: string]: any } {
|
||||||
|
const filters = { ...this.currentExtraParams }
|
||||||
|
if (this._nameFilter?.length) {
|
||||||
|
filters['name__icontains'] = this._nameFilter
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
|
this.allSelectionActive = false
|
||||||
this.togggleAll = false
|
this.togggleAll = false
|
||||||
this.selectedObjects.clear()
|
this.selectedObjects.clear()
|
||||||
}
|
}
|
||||||
@@ -356,6 +376,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectPage() {
|
selectPage() {
|
||||||
|
this.allSelectionActive = false
|
||||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||||
this.togggleAll = this.areAllPageItemsSelected()
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
}
|
}
|
||||||
@@ -365,11 +386,16 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.selectedObjects = new Set(this.allIDs)
|
|
||||||
|
this.allSelectionActive = true
|
||||||
|
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||||
this.togggleAll = this.areAllPageItemsSelected()
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelected(object) {
|
toggleSelected(object) {
|
||||||
|
if (this.allSelectionActive) {
|
||||||
|
this.allSelectionActive = false
|
||||||
|
}
|
||||||
this.selectedObjects.has(object.id)
|
this.selectedObjects.has(object.id)
|
||||||
? this.selectedObjects.delete(object.id)
|
? this.selectedObjects.delete(object.id)
|
||||||
: this.selectedObjects.add(object.id)
|
: this.selectedObjects.add(object.id)
|
||||||
@@ -377,6 +403,9 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected areAllPageItemsSelected(): boolean {
|
protected areAllPageItemsSelected(): boolean {
|
||||||
|
if (this.allSelectionActive) {
|
||||||
|
return this.data.length > 0
|
||||||
|
}
|
||||||
const ids = this.getSelectableIDs(this.data)
|
const ids = this.getSelectableIDs(this.data)
|
||||||
return ids.length > 0 && ids.every((id) => this.selectedObjects.has(id))
|
return ids.length > 0 && ids.every((id) => this.selectedObjects.has(id))
|
||||||
}
|
}
|
||||||
@@ -390,10 +419,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.service
|
this.service
|
||||||
.bulk_edit_objects(
|
.bulk_edit_objects(
|
||||||
Array.from(this.selectedObjects),
|
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
||||||
BulkEditObjectOperation.SetPermissions,
|
BulkEditObjectOperation.SetPermissions,
|
||||||
permissions,
|
permissions,
|
||||||
merge
|
merge,
|
||||||
|
this.allSelectionActive,
|
||||||
|
this.allSelectionActive ? this.getBulkEditFilters() : null
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -428,8 +459,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.service
|
this.service
|
||||||
.bulk_edit_objects(
|
.bulk_edit_objects(
|
||||||
Array.from(this.selectedObjects),
|
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
||||||
BulkEditObjectOperation.Delete
|
BulkEditObjectOperation.Delete,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
this.allSelectionActive,
|
||||||
|
this.allSelectionActive ? this.getBulkEditFilters() : null
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ describe('TagListComponent', () => {
|
|||||||
listFilteredSpy = jest.spyOn(tagService, 'listFiltered').mockReturnValue(
|
listFilteredSpy = jest.spyOn(tagService, 'listFiltered').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
count: 3,
|
count: 3,
|
||||||
all: [1, 2, 3],
|
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TagEditDialogComponent } from 'src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from 'src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
import { Results } from 'src/app/data/results'
|
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
@@ -77,16 +76,6 @@ export class TagListComponent extends ManagementListComponent<Tag> {
|
|||||||
return data.filter((tag) => !tag.parent || !availableIds.has(tag.parent))
|
return data.filter((tag) => !tag.parent || !availableIds.has(tag.parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override getCollectionSize(results: Results<Tag>): number {
|
|
||||||
// Tag list pages are requested with is_root=true (when unfiltered), so
|
|
||||||
// pagination must follow root count even though `all` includes descendants
|
|
||||||
return results.count
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getDisplayCollectionSize(results: Results<Tag>): number {
|
|
||||||
return super.getCollectionSize(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getSelectableIDs(tags: Tag[]): number[] {
|
protected override getSelectableIDs(tags: Tag[]): number[] {
|
||||||
const ids: number[] = []
|
const ids: number[] = []
|
||||||
for (const tag of tags.filter(Boolean)) {
|
for (const tag of tags.filter(Boolean)) {
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
|
import { Document } from './document'
|
||||||
|
|
||||||
export interface Results<T> {
|
export interface Results<T> {
|
||||||
count: number
|
count: number
|
||||||
|
|
||||||
results: T[]
|
display_count?: number
|
||||||
|
|
||||||
all: number[]
|
results: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionDataItem {
|
||||||
|
id: number
|
||||||
|
document_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionData {
|
||||||
|
selected_storage_paths: SelectionDataItem[]
|
||||||
|
selected_correspondents: SelectionDataItem[]
|
||||||
|
selected_tags: SelectionDataItem[]
|
||||||
|
selected_document_types: SelectionDataItem[]
|
||||||
|
selected_custom_fields: SelectionDataItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentResults extends Results<Document> {
|
||||||
|
selection_data?: SelectionData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,13 +126,10 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
documentListViewService.reload()
|
documentListViewService.reload()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.isReloading).toBeFalsy()
|
expect(documentListViewService.isReloading).toBeFalsy()
|
||||||
expect(documentListViewService.activeSavedViewId).toBeNull()
|
expect(documentListViewService.activeSavedViewId).toBeNull()
|
||||||
@@ -144,12 +141,12 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should handle error on page request out of range', () => {
|
it('should handle error on page request out of range', () => {
|
||||||
documentListViewService.currentPage = 50
|
documentListViewService.currentPage = 50
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=50&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=50&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush([], { status: 404, statusText: 'Unexpected error' })
|
req.flush([], { status: 404, statusText: 'Unexpected error' })
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
@@ -166,7 +163,7 @@ describe('DocumentListViewService', () => {
|
|||||||
]
|
]
|
||||||
documentListViewService.setFilterRules(filterRulesAny)
|
documentListViewService.setFilterRules(filterRulesAny)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__in=${tags__id__in}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush(
|
req.flush(
|
||||||
@@ -174,13 +171,13 @@ describe('DocumentListViewService', () => {
|
|||||||
{ status: 404, statusText: 'Unexpected error' }
|
{ status: 404, statusText: 'Unexpected error' }
|
||||||
)
|
)
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.setFilterRules([])
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -188,7 +185,7 @@ describe('DocumentListViewService', () => {
|
|||||||
documentListViewService.currentPage = 1
|
documentListViewService.currentPage = 1
|
||||||
documentListViewService.sortField = 'custom_field_999'
|
documentListViewService.sortField = 'custom_field_999'
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-custom_field_999&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-custom_field_999&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush(
|
req.flush(
|
||||||
@@ -197,7 +194,7 @@ describe('DocumentListViewService', () => {
|
|||||||
)
|
)
|
||||||
// resets itself
|
// resets itself
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,7 +209,7 @@ describe('DocumentListViewService', () => {
|
|||||||
]
|
]
|
||||||
documentListViewService.setFilterRules(filterRulesAny)
|
documentListViewService.setFilterRules(filterRulesAny)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__in=${tags__id__in}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
|
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
|
||||||
@@ -220,7 +217,7 @@ describe('DocumentListViewService', () => {
|
|||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.setFilterRules([])
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -229,7 +226,7 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(documentListViewService.sortReverse).toBeTruthy()
|
expect(documentListViewService.sortReverse).toBeTruthy()
|
||||||
documentListViewService.setSort('added', false)
|
documentListViewService.setSort('added', false)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=added&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=added&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.sortField).toEqual('added')
|
expect(documentListViewService.sortField).toEqual('added')
|
||||||
@@ -237,12 +234,12 @@ describe('DocumentListViewService', () => {
|
|||||||
|
|
||||||
documentListViewService.sortField = 'created'
|
documentListViewService.sortField = 'created'
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(documentListViewService.sortField).toEqual('created')
|
expect(documentListViewService.sortField).toEqual('created')
|
||||||
documentListViewService.sortReverse = true
|
documentListViewService.sortReverse = true
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.sortReverse).toBeTruthy()
|
expect(documentListViewService.sortReverse).toBeTruthy()
|
||||||
@@ -262,7 +259,7 @@ describe('DocumentListViewService', () => {
|
|||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
|
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
|
||||||
documentListViewService.pageSize
|
documentListViewService.pageSize
|
||||||
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true`
|
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.currentPage).toEqual(page)
|
expect(documentListViewService.currentPage).toEqual(page)
|
||||||
@@ -279,7 +276,7 @@ describe('DocumentListViewService', () => {
|
|||||||
}
|
}
|
||||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&include_selection_data=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.filterRules).toEqual([
|
expect(documentListViewService.filterRules).toEqual([
|
||||||
@@ -289,15 +286,12 @@ describe('DocumentListViewService', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use filter rules to update query params', () => {
|
it('should use filter rules to update query params', () => {
|
||||||
documentListViewService.setFilterRules(filterRules)
|
documentListViewService.setFilterRules(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
@@ -306,34 +300,26 @@ describe('DocumentListViewService', () => {
|
|||||||
documentListViewService.currentPage = 2
|
documentListViewService.currentPage = 2
|
||||||
let req = httpTestingController.expectOne((request) =>
|
let req = httpTestingController.expectOne((request) =>
|
||||||
request.urlWithParams.startsWith(
|
request.urlWithParams.startsWith(
|
||||||
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
req.flush([])
|
|
||||||
|
|
||||||
documentListViewService.setFilterRules(filterRules, true)
|
documentListViewService.setFilterRules(filterRules, true)
|
||||||
|
|
||||||
const filteredReqs = httpTestingController.match(
|
const filteredReqs = httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(filteredReqs).toHaveLength(1)
|
expect(filteredReqs).toHaveLength(1)
|
||||||
filteredReqs[0].flush(full_results)
|
filteredReqs[0].flush(full_results)
|
||||||
req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
req.flush([])
|
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support quick filter', () => {
|
it('should support quick filter', () => {
|
||||||
documentListViewService.quickFilter(filterRules)
|
documentListViewService.quickFilter(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
@@ -356,21 +342,21 @@ describe('DocumentListViewService', () => {
|
|||||||
convertToParamMap(params)
|
convertToParamMap(params)
|
||||||
)
|
)
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&include_selection_data=true&tags__id__all=${tags__id__all}`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
// reset the list
|
// reset the list
|
||||||
documentListViewService.currentPage = 1
|
documentListViewService.currentPage = 1
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&include_selection_data=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
documentListViewService.setFilterRules([])
|
documentListViewService.setFilterRules([])
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
documentListViewService.sortField = 'created'
|
documentListViewService.sortField = 'created'
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
documentListViewService.activateSavedView(null)
|
documentListViewService.activateSavedView(null)
|
||||||
})
|
})
|
||||||
@@ -378,21 +364,18 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should support navigating next / previous', () => {
|
it('should support navigating next / previous', () => {
|
||||||
documentListViewService.setFilterRules([])
|
documentListViewService.setFilterRules([])
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
documentListViewService.pageSize = 3
|
documentListViewService.pageSize = 3
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush({
|
req.flush({
|
||||||
count: 3,
|
count: 3,
|
||||||
results: documents.slice(0, 3),
|
results: documents.slice(0, 3),
|
||||||
})
|
})
|
||||||
httpTestingController
|
|
||||||
.expectOne(`${environment.apiBaseUrl}documents/selection_data/`)
|
|
||||||
.flush([])
|
|
||||||
expect(documentListViewService.hasNext(documents[0].id)).toBeTruthy()
|
expect(documentListViewService.hasNext(documents[0].id)).toBeTruthy()
|
||||||
expect(documentListViewService.hasPrevious(documents[0].id)).toBeFalsy()
|
expect(documentListViewService.hasPrevious(documents[0].id)).toBeFalsy()
|
||||||
documentListViewService.getNext(documents[0].id).subscribe((docId) => {
|
documentListViewService.getNext(documents[0].id).subscribe((docId) => {
|
||||||
@@ -439,7 +422,7 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
documentListViewService.pageSize = 3
|
documentListViewService.pageSize = 3
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
jest
|
jest
|
||||||
.spyOn(documentListViewService, 'getLastPage')
|
.spyOn(documentListViewService, 'getLastPage')
|
||||||
@@ -454,7 +437,7 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(reloadSpy).toHaveBeenCalled()
|
expect(reloadSpy).toHaveBeenCalled()
|
||||||
expect(documentListViewService.currentPage).toEqual(2)
|
expect(documentListViewService.currentPage).toEqual(2)
|
||||||
const reqs = httpTestingController.match(
|
const reqs = httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(reqs.length).toBeGreaterThan(0)
|
expect(reqs.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -489,11 +472,11 @@ describe('DocumentListViewService', () => {
|
|||||||
.mockReturnValue(documents)
|
.mockReturnValue(documents)
|
||||||
documentListViewService.currentPage = 2
|
documentListViewService.currentPage = 2
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
documentListViewService.pageSize = 3
|
documentListViewService.pageSize = 3
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
const reloadSpy = jest.spyOn(documentListViewService, 'reload')
|
const reloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||||
documentListViewService.getPrevious(1).subscribe({
|
documentListViewService.getPrevious(1).subscribe({
|
||||||
@@ -503,7 +486,7 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(reloadSpy).toHaveBeenCalled()
|
expect(reloadSpy).toHaveBeenCalled()
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
const reqs = httpTestingController.match(
|
const reqs = httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(reqs.length).toBeGreaterThan(0)
|
expect(reqs.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -516,13 +499,10 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should support select a document', () => {
|
it('should support select a document', () => {
|
||||||
documentListViewService.reload()
|
documentListViewService.reload()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
documentListViewService.toggleSelected(documents[0])
|
documentListViewService.toggleSelected(documents[0])
|
||||||
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
|
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
|
||||||
documentListViewService.toggleSelected(documents[0])
|
documentListViewService.toggleSelected(documents[0])
|
||||||
@@ -544,16 +524,13 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should support select page', () => {
|
it('should support select page', () => {
|
||||||
documentListViewService.pageSize = 3
|
documentListViewService.pageSize = 3
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush({
|
req.flush({
|
||||||
count: 3,
|
count: 3,
|
||||||
results: documents.slice(0, 3),
|
results: documents.slice(0, 3),
|
||||||
})
|
})
|
||||||
httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
documentListViewService.selectPage()
|
documentListViewService.selectPage()
|
||||||
expect(documentListViewService.selected.size).toEqual(3)
|
expect(documentListViewService.selected.size).toEqual(3)
|
||||||
expect(documentListViewService.isSelected(documents[5])).toBeFalsy()
|
expect(documentListViewService.isSelected(documents[5])).toBeFalsy()
|
||||||
@@ -562,13 +539,10 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should support select range', () => {
|
it('should support select range', () => {
|
||||||
documentListViewService.reload()
|
documentListViewService.reload()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
req.flush(full_results)
|
req.flush(full_results)
|
||||||
httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/selection_data/`
|
|
||||||
)
|
|
||||||
documentListViewService.toggleSelected(documents[0])
|
documentListViewService.toggleSelected(documents[0])
|
||||||
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
|
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
|
||||||
documentListViewService.selectRangeTo(documents[2])
|
documentListViewService.selectRangeTo(documents[2])
|
||||||
@@ -588,7 +562,7 @@ describe('DocumentListViewService', () => {
|
|||||||
|
|
||||||
documentListViewService.setFilterRules(filterRules)
|
documentListViewService.setFilterRules(filterRules)
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
const reqs = httpTestingController.match(
|
const reqs = httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id&tags__id__all=9`
|
||||||
@@ -604,7 +578,7 @@ describe('DocumentListViewService', () => {
|
|||||||
const cancelSpy = jest.spyOn(documentListViewService, 'cancelPending')
|
const cancelSpy = jest.spyOn(documentListViewService, 'cancelPending')
|
||||||
documentListViewService.reload()
|
documentListViewService.reload()
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=9`
|
||||||
)
|
)
|
||||||
expect(cancelSpy).toHaveBeenCalled()
|
expect(cancelSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -623,7 +597,7 @@ describe('DocumentListViewService', () => {
|
|||||||
documentListViewService.setFilterRules([])
|
documentListViewService.setFilterRules([])
|
||||||
expect(documentListViewService.sortField).toEqual('created')
|
expect(documentListViewService.sortField).toEqual('created')
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -650,11 +624,11 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(localStorageSpy).toHaveBeenCalled()
|
expect(localStorageSpy).toHaveBeenCalled()
|
||||||
// reload triggered
|
// reload triggered
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
documentListViewService.displayFields = null
|
documentListViewService.displayFields = null
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
expect(documentListViewService.displayFields).toEqual(
|
expect(documentListViewService.displayFields).toEqual(
|
||||||
DEFAULT_DISPLAY_FIELDS.filter((f) => f.id !== DisplayField.ADDED).map(
|
DEFAULT_DISPLAY_FIELDS.filter((f) => f.id !== DisplayField.ADDED).map(
|
||||||
@@ -694,7 +668,7 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should generate quick filter URL preserving default state', () => {
|
it('should generate quick filter URL preserving default state', () => {
|
||||||
documentListViewService.reload()
|
documentListViewService.reload()
|
||||||
httpTestingController.expectOne(
|
httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||||
)
|
)
|
||||||
const urlTree = documentListViewService.getQuickFilterUrl(filterRules)
|
const urlTree = documentListViewService.getQuickFilterUrl(filterRules)
|
||||||
expect(urlTree).toBeDefined()
|
expect(urlTree).toBeDefined()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, inject } from '@angular/core'
|
import { Injectable, inject } from '@angular/core'
|
||||||
import { ParamMap, Router, UrlTree } from '@angular/router'
|
import { ParamMap, Router, UrlTree } from '@angular/router'
|
||||||
import { Observable, Subject, first, takeUntil } from 'rxjs'
|
import { Observable, Subject, takeUntil } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
DEFAULT_DISPLAY_FIELDS,
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
DisplayField,
|
DisplayField,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Document,
|
Document,
|
||||||
} from '../data/document'
|
} from '../data/document'
|
||||||
import { FilterRule } from '../data/filter-rule'
|
import { FilterRule } from '../data/filter-rule'
|
||||||
|
import { DocumentResults, SelectionData } from '../data/results'
|
||||||
import { SavedView } from '../data/saved-view'
|
import { SavedView } from '../data/saved-view'
|
||||||
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
||||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
isFullTextFilterRule,
|
isFullTextFilterRule,
|
||||||
} from '../utils/filter-rules'
|
} from '../utils/filter-rules'
|
||||||
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
|
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
|
||||||
import { DocumentService, SelectionData } from './rest/document.service'
|
import { DocumentService } from './rest/document.service'
|
||||||
import { SettingsService } from './settings.service'
|
import { SettingsService } from './settings.service'
|
||||||
|
|
||||||
const LIST_DEFAULT_DISPLAY_FIELDS: DisplayField[] = DEFAULT_DISPLAY_FIELDS.map(
|
const LIST_DEFAULT_DISPLAY_FIELDS: DisplayField[] = DEFAULT_DISPLAY_FIELDS.map(
|
||||||
@@ -260,27 +261,17 @@ export class DocumentListViewService {
|
|||||||
activeListViewState.sortField,
|
activeListViewState.sortField,
|
||||||
activeListViewState.sortReverse,
|
activeListViewState.sortReverse,
|
||||||
activeListViewState.filterRules,
|
activeListViewState.filterRules,
|
||||||
{ truncate_content: true }
|
{ truncate_content: true, include_selection_data: true }
|
||||||
)
|
)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
|
const resultWithSelectionData = result as DocumentResults
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
this.isReloading = false
|
this.isReloading = false
|
||||||
activeListViewState.collectionSize = result.count
|
activeListViewState.collectionSize = result.count
|
||||||
activeListViewState.documents = result.results
|
activeListViewState.documents = result.results
|
||||||
|
this.selectionData = resultWithSelectionData.selection_data ?? null
|
||||||
this.documentService
|
|
||||||
.getSelectionData(result.all)
|
|
||||||
.pipe(first())
|
|
||||||
.subscribe({
|
|
||||||
next: (selectionData) => {
|
|
||||||
this.selectionData = selectionData
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.selectionData = null
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (updateQueryParams && !this._activeSavedViewId) {
|
if (updateQueryParams && !this._activeSavedViewId) {
|
||||||
let base = ['/documents']
|
let base = ['/documents']
|
||||||
|
|||||||
@@ -96,6 +96,30 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
|
|||||||
})
|
})
|
||||||
req.flush([])
|
req.flush([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should call appropriate api endpoint for bulk delete on all filtered objects', () => {
|
||||||
|
subscription = service
|
||||||
|
.bulk_edit_objects(
|
||||||
|
[],
|
||||||
|
BulkEditObjectOperation.Delete,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
{ name__icontains: 'hello' }
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}bulk_edit_objects/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('POST')
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
object_type: endpoint,
|
||||||
|
operation: BulkEditObjectOperation.Delete,
|
||||||
|
all: true,
|
||||||
|
filters: { name__icontains: 'hello' },
|
||||||
|
})
|
||||||
|
req.flush([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -37,13 +37,22 @@ export abstract class AbstractNameFilterService<
|
|||||||
objects: Array<number>,
|
objects: Array<number>,
|
||||||
operation: BulkEditObjectOperation,
|
operation: BulkEditObjectOperation,
|
||||||
permissions: { owner: number; set_permissions: PermissionsObject } = null,
|
permissions: { owner: number; set_permissions: PermissionsObject } = null,
|
||||||
merge: boolean = null
|
merge: boolean = null,
|
||||||
|
all: boolean = false,
|
||||||
|
filters: { [key: string]: any } = null
|
||||||
): Observable<string> {
|
): Observable<string> {
|
||||||
const params = {
|
const params: any = {
|
||||||
objects,
|
|
||||||
object_type: this.resourceName,
|
object_type: this.resourceName,
|
||||||
operation,
|
operation,
|
||||||
}
|
}
|
||||||
|
if (all) {
|
||||||
|
params['all'] = true
|
||||||
|
if (filters) {
|
||||||
|
params['filters'] = filters
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params['objects'] = objects
|
||||||
|
}
|
||||||
if (operation === BulkEditObjectOperation.SetPermissions) {
|
if (operation === BulkEditObjectOperation.SetPermissions) {
|
||||||
params['owner'] = permissions?.owner
|
params['owner'] = permissions?.owner
|
||||||
params['permissions'] = permissions?.set_permissions
|
params['permissions'] = permissions?.set_permissions
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { DocumentMetadata } from 'src/app/data/document-metadata'
|
import { DocumentMetadata } from 'src/app/data/document-metadata'
|
||||||
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
||||||
import { FilterRule } from 'src/app/data/filter-rule'
|
import { FilterRule } from 'src/app/data/filter-rule'
|
||||||
import { Results } from 'src/app/data/results'
|
import { Results, SelectionData } from 'src/app/data/results'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { queryParamsFromFilterRules } from '../../utils/query-params'
|
import { queryParamsFromFilterRules } from '../../utils/query-params'
|
||||||
import {
|
import {
|
||||||
@@ -24,19 +24,6 @@ import { SettingsService } from '../settings.service'
|
|||||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||||
import { CustomFieldsService } from './custom-fields.service'
|
import { CustomFieldsService } from './custom-fields.service'
|
||||||
|
|
||||||
export interface SelectionDataItem {
|
|
||||||
id: number
|
|
||||||
document_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectionData {
|
|
||||||
selected_storage_paths: SelectionDataItem[]
|
|
||||||
selected_correspondents: SelectionDataItem[]
|
|
||||||
selected_tags: SelectionDataItem[]
|
|
||||||
selected_document_types: SelectionDataItem[]
|
|
||||||
selected_custom_fields: SelectionDataItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BulkEditSourceMode {
|
export enum BulkEditSourceMode {
|
||||||
LATEST_VERSION = 'latest_version',
|
LATEST_VERSION = 'latest_version',
|
||||||
EXPLICIT_SELECTION = 'explicit_selection',
|
EXPLICIT_SELECTION = 'explicit_selection',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBaseUrl: 'http://localhost:8000/api/',
|
apiBaseUrl: 'http://localhost:8000/api/',
|
||||||
apiVersion: '9',
|
apiVersion: '10',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
tag: 'dev',
|
tag: 'dev',
|
||||||
version: 'DEVELOPMENT',
|
version: 'DEVELOPMENT',
|
||||||
|
|||||||
@@ -375,6 +375,26 @@ class DelayedQuery:
|
|||||||
]
|
]
|
||||||
return self._manual_hits_cache
|
return self._manual_hits_cache
|
||||||
|
|
||||||
|
def get_result_ids(self) -> list[int]:
|
||||||
|
"""
|
||||||
|
Return all matching document IDs for the current query and ordering.
|
||||||
|
"""
|
||||||
|
if self._manual_sort_requested():
|
||||||
|
return [hit["id"] for hit in self._manual_hits()]
|
||||||
|
|
||||||
|
q, mask, suggested_correction = self._get_query()
|
||||||
|
self.suggested_correction = suggested_correction
|
||||||
|
sortedby, reverse = self._get_query_sortedby()
|
||||||
|
results = self.searcher.search(
|
||||||
|
q,
|
||||||
|
mask=mask,
|
||||||
|
filter=MappedDocIdSet(self.filter_queryset, self.searcher.ixreader),
|
||||||
|
limit=None,
|
||||||
|
sortedby=sortedby,
|
||||||
|
reverse=reverse,
|
||||||
|
)
|
||||||
|
return [hit["id"] for hit in results]
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
if item.start in self.saved_results:
|
if item.start in self.saved_results:
|
||||||
return self.saved_results[item.start]
|
return self.saved_results[item.start]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0003_workflowaction_order"),
|
("documents", "0002_squashed"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.2.9 on 2026-01-20 20:06
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("documents", "0002_squashed"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="workflowaction",
|
|
||||||
name="order",
|
|
||||||
field=models.PositiveIntegerField(default=0, verbose_name="order"),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0004_remove_document_storage_type"),
|
("documents", "0003_remove_document_storage_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0005_workflowtrigger_filter_has_any_correspondents_and_more"),
|
("documents", "0004_workflowtrigger_filter_has_any_correspondents_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -7,7 +7,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0006_alter_document_checksum_unique"),
|
("documents", "0005_alter_document_checksum_unique"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -46,7 +46,7 @@ def revoke_share_link_bundle_permissions(apps, schema_editor):
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
("documents", "0007_document_content_length"),
|
("documents", "0006_document_content_length"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0008_sharelinkbundle"),
|
("documents", "0007_sharelinkbundle"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -7,7 +7,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0009_workflowaction_passwords_alter_workflowaction_type"),
|
("documents", "0008_workflowaction_passwords_alter_workflowaction_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -7,7 +7,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0010_alter_document_content_length"),
|
("documents", "0009_alter_document_content_length"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0011_optimize_integer_field_sizes"),
|
("documents", "0010_optimize_integer_field_sizes"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -7,7 +7,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0012_alter_workflowaction_type"),
|
("documents", "0011_alter_workflowaction_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0013_document_root_document"),
|
("documents", "0012_document_root_document"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -124,7 +124,7 @@ def _restore_visibility_fields(apps, schema_editor):
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0014_alter_paperlesstask_task_name"),
|
("documents", "0013_alter_paperlesstask_task_name"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -7,7 +7,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0015_savedview_visibility_to_ui_settings"),
|
("documents", "0014_savedview_visibility_to_ui_settings"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2571,13 +2571,25 @@ class ShareLinkBundleSerializer(OwnedObjectSerializer):
|
|||||||
|
|
||||||
class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
||||||
objects = serializers.ListField(
|
objects = serializers.ListField(
|
||||||
required=True,
|
required=False,
|
||||||
allow_empty=False,
|
allow_empty=True,
|
||||||
label="Objects",
|
label="Objects",
|
||||||
write_only=True,
|
write_only=True,
|
||||||
child=serializers.IntegerField(),
|
child=serializers.IntegerField(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
all = serializers.BooleanField(
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
write_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = serializers.DictField(
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
write_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
object_type = serializers.ChoiceField(
|
object_type = serializers.ChoiceField(
|
||||||
choices=[
|
choices=[
|
||||||
"tags",
|
"tags",
|
||||||
@@ -2650,10 +2662,20 @@ class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
|||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
object_type = attrs["object_type"]
|
object_type = attrs["object_type"]
|
||||||
objects = attrs["objects"]
|
objects = attrs.get("objects")
|
||||||
|
apply_to_all = attrs.get("all", False)
|
||||||
operation = attrs.get("operation")
|
operation = attrs.get("operation")
|
||||||
|
|
||||||
self._validate_objects(objects, object_type)
|
if apply_to_all:
|
||||||
|
attrs.setdefault("objects", [])
|
||||||
|
else:
|
||||||
|
if objects is None:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"objects is required unless all is true.",
|
||||||
|
)
|
||||||
|
if len(objects) == 0:
|
||||||
|
raise serializers.ValidationError("objects must not be empty")
|
||||||
|
self._validate_objects(objects, object_type)
|
||||||
|
|
||||||
if operation == "set_permissions":
|
if operation == "set_permissions":
|
||||||
permissions = attrs.get("permissions")
|
permissions = attrs.get("permissions")
|
||||||
|
|||||||
@@ -1119,21 +1119,19 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
[u1_doc1.id],
|
[u1_doc1.id],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_pagination_all(self) -> None:
|
def test_pagination_results(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- A set of 50 documents
|
- A set of 50 documents
|
||||||
WHEN:
|
WHEN:
|
||||||
- API request for document filtering
|
- API request for document filtering
|
||||||
THEN:
|
THEN:
|
||||||
- Results are paginated (25 items) and response["all"] returns all ids (50 items)
|
- Results are paginated (25 items) and count reflects all results (50 items)
|
||||||
"""
|
"""
|
||||||
t = Tag.objects.create(name="tag")
|
t = Tag.objects.create(name="tag")
|
||||||
docs = []
|
|
||||||
for i in range(50):
|
for i in range(50):
|
||||||
d = Document.objects.create(checksum=i, content=f"test{i}")
|
d = Document.objects.create(checksum=i, content=f"test{i}")
|
||||||
d.tags.add(t)
|
d.tags.add(t)
|
||||||
docs.append(d)
|
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
f"/api/documents/?tags__id__in={t.id}",
|
f"/api/documents/?tags__id__in={t.id}",
|
||||||
@@ -1141,9 +1139,84 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
self.assertEqual(len(results), 25)
|
self.assertEqual(len(results), 25)
|
||||||
self.assertEqual(len(response.data["all"]), 50)
|
self.assertEqual(response.data["count"], 50)
|
||||||
|
self.assertNotIn("all", response.data)
|
||||||
|
|
||||||
|
def test_pagination_all_for_api_version_9(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A set of documents matching a filter
|
||||||
|
WHEN:
|
||||||
|
- API request uses legacy version 9
|
||||||
|
THEN:
|
||||||
|
- Response includes "all" for backward compatibility
|
||||||
|
"""
|
||||||
|
t = Tag.objects.create(name="tag")
|
||||||
|
docs = []
|
||||||
|
for i in range(4):
|
||||||
|
d = Document.objects.create(checksum=i, content=f"test{i}")
|
||||||
|
d.tags.add(t)
|
||||||
|
docs.append(d)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
f"/api/documents/?tags__id__in={t.id}",
|
||||||
|
headers={"Accept": "application/json; version=9"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("all", response.data)
|
||||||
self.assertCountEqual(response.data["all"], [d.id for d in docs])
|
self.assertCountEqual(response.data["all"], [d.id for d in docs])
|
||||||
|
|
||||||
|
def test_list_with_include_selection_data(self) -> None:
|
||||||
|
correspondent = Correspondent.objects.create(name="c1")
|
||||||
|
doc_type = DocumentType.objects.create(name="dt1")
|
||||||
|
storage_path = StoragePath.objects.create(name="sp1")
|
||||||
|
tag = Tag.objects.create(name="tag")
|
||||||
|
|
||||||
|
matching_doc = Document.objects.create(
|
||||||
|
checksum="A",
|
||||||
|
correspondent=correspondent,
|
||||||
|
document_type=doc_type,
|
||||||
|
storage_path=storage_path,
|
||||||
|
)
|
||||||
|
matching_doc.tags.add(tag)
|
||||||
|
|
||||||
|
non_matching_doc = Document.objects.create(checksum="B")
|
||||||
|
non_matching_doc.tags.add(Tag.objects.create(name="other"))
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
f"/api/documents/?tags__id__in={tag.id}&include_selection_data=true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("selection_data", response.data)
|
||||||
|
|
||||||
|
selected_correspondent = next(
|
||||||
|
item
|
||||||
|
for item in response.data["selection_data"]["selected_correspondents"]
|
||||||
|
if item["id"] == correspondent.id
|
||||||
|
)
|
||||||
|
selected_tag = next(
|
||||||
|
item
|
||||||
|
for item in response.data["selection_data"]["selected_tags"]
|
||||||
|
if item["id"] == tag.id
|
||||||
|
)
|
||||||
|
selected_type = next(
|
||||||
|
item
|
||||||
|
for item in response.data["selection_data"]["selected_document_types"]
|
||||||
|
if item["id"] == doc_type.id
|
||||||
|
)
|
||||||
|
selected_storage_path = next(
|
||||||
|
item
|
||||||
|
for item in response.data["selection_data"]["selected_storage_paths"]
|
||||||
|
if item["id"] == storage_path.id
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(selected_correspondent["document_count"], 1)
|
||||||
|
self.assertEqual(selected_tag["document_count"], 1)
|
||||||
|
self.assertEqual(selected_type["document_count"], 1)
|
||||||
|
self.assertEqual(selected_storage_path["document_count"], 1)
|
||||||
|
|
||||||
def test_statistics(self) -> None:
|
def test_statistics(self) -> None:
|
||||||
doc1 = Document.objects.create(
|
doc1 = Document.objects.create(
|
||||||
title="none1",
|
title="none1",
|
||||||
|
|||||||
@@ -145,6 +145,22 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
response.data["last_correspondence"],
|
response.data["last_correspondence"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_paginated_objects_include_all_only_for_legacy_version(self) -> None:
|
||||||
|
response_v10 = self.client.get("/api/correspondents/")
|
||||||
|
self.assertEqual(response_v10.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertNotIn("all", response_v10.data)
|
||||||
|
|
||||||
|
response_v9 = self.client.get(
|
||||||
|
"/api/correspondents/",
|
||||||
|
headers={"Accept": "application/json; version=9"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response_v9.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("all", response_v9.data)
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_v9.data["all"],
|
||||||
|
[self.c1.id, self.c2.id, self.c3.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
||||||
ENDPOINT = "/api/storage_paths/"
|
ENDPOINT = "/api/storage_paths/"
|
||||||
@@ -774,6 +790,62 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(StoragePath.objects.count(), 0)
|
self.assertEqual(StoragePath.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_bulk_objects_delete_all_filtered(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects that can be filtered by name
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with all=true and filters
|
||||||
|
THEN:
|
||||||
|
- Matching objects are deleted without passing explicit IDs
|
||||||
|
"""
|
||||||
|
Correspondent.objects.create(name="c2")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"all": True,
|
||||||
|
"filters": {"name__icontains": "c"},
|
||||||
|
"object_type": "correspondents",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(Correspondent.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_bulk_objects_delete_all_filtered_tags_includes_descendants(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Root tag with descendants
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with all=true
|
||||||
|
THEN:
|
||||||
|
- Root tags and descendants are deleted
|
||||||
|
"""
|
||||||
|
parent = Tag.objects.create(name="parent")
|
||||||
|
child = Tag.objects.create(name="child", tn_parent=parent)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"all": True,
|
||||||
|
"filters": {"is_root": True},
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertFalse(Tag.objects.filter(id=parent.id).exists())
|
||||||
|
self.assertFalse(Tag.objects.filter(id=child.id).exists())
|
||||||
|
|
||||||
def test_bulk_edit_object_permissions_insufficient_global_perms(self) -> None:
|
def test_bulk_edit_object_permissions_insufficient_global_perms(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@@ -861,3 +933,40 @@ class TestBulkEditObjects(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertEqual(response.content, b"Insufficient permissions")
|
self.assertEqual(response.content, b"Insufficient permissions")
|
||||||
|
|
||||||
|
def test_bulk_edit_all_filtered_permissions_insufficient_object_perms(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Filter-matching objects include one that the user cannot edit
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with all=true
|
||||||
|
THEN:
|
||||||
|
- Operation applies only to editable objects
|
||||||
|
"""
|
||||||
|
self.t2.owner = User.objects.get(username="temp_admin")
|
||||||
|
self.t2.save()
|
||||||
|
|
||||||
|
self.user1.user_permissions.add(
|
||||||
|
*Permission.objects.filter(codename="delete_tag"),
|
||||||
|
)
|
||||||
|
self.user1.save()
|
||||||
|
self.client.force_authenticate(user=self.user1)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"all": True,
|
||||||
|
"filters": {"name__icontains": "t"},
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertTrue(Tag.objects.filter(id=self.t2.id).exists())
|
||||||
|
self.assertFalse(Tag.objects.filter(id=self.t1.id).exists())
|
||||||
|
|||||||
@@ -68,26 +68,88 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
|||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
self.assertEqual(response.data["count"], 3)
|
self.assertEqual(response.data["count"], 3)
|
||||||
self.assertEqual(len(results), 3)
|
self.assertEqual(len(results), 3)
|
||||||
self.assertCountEqual(response.data["all"], [d1.id, d2.id, d3.id])
|
|
||||||
|
|
||||||
response = self.client.get("/api/documents/?query=september")
|
response = self.client.get("/api/documents/?query=september")
|
||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
self.assertEqual(response.data["count"], 1)
|
self.assertEqual(response.data["count"], 1)
|
||||||
self.assertEqual(len(results), 1)
|
self.assertEqual(len(results), 1)
|
||||||
self.assertCountEqual(response.data["all"], [d3.id])
|
|
||||||
self.assertEqual(results[0]["original_file_name"], "someepdf.pdf")
|
self.assertEqual(results[0]["original_file_name"], "someepdf.pdf")
|
||||||
|
|
||||||
response = self.client.get("/api/documents/?query=statement")
|
response = self.client.get("/api/documents/?query=statement")
|
||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
self.assertEqual(response.data["count"], 2)
|
self.assertEqual(response.data["count"], 2)
|
||||||
self.assertEqual(len(results), 2)
|
self.assertEqual(len(results), 2)
|
||||||
self.assertCountEqual(response.data["all"], [d2.id, d3.id])
|
|
||||||
|
|
||||||
response = self.client.get("/api/documents/?query=sfegdfg")
|
response = self.client.get("/api/documents/?query=sfegdfg")
|
||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
self.assertEqual(response.data["count"], 0)
|
self.assertEqual(response.data["count"], 0)
|
||||||
self.assertEqual(len(results), 0)
|
self.assertEqual(len(results), 0)
|
||||||
self.assertCountEqual(response.data["all"], [])
|
|
||||||
|
def test_search_returns_all_for_api_version_9(self) -> None:
|
||||||
|
d1 = Document.objects.create(
|
||||||
|
title="invoice",
|
||||||
|
content="bank payment",
|
||||||
|
checksum="A",
|
||||||
|
pk=1,
|
||||||
|
)
|
||||||
|
d2 = Document.objects.create(
|
||||||
|
title="bank statement",
|
||||||
|
content="bank transfer",
|
||||||
|
checksum="B",
|
||||||
|
pk=2,
|
||||||
|
)
|
||||||
|
with AsyncWriter(index.open_index()) as writer:
|
||||||
|
index.update_document(writer, d1)
|
||||||
|
index.update_document(writer, d2)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/documents/?query=bank",
|
||||||
|
headers={"Accept": "application/json; version=9"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("all", response.data)
|
||||||
|
self.assertCountEqual(response.data["all"], [d1.id, d2.id])
|
||||||
|
|
||||||
|
def test_search_with_include_selection_data(self) -> None:
|
||||||
|
correspondent = Correspondent.objects.create(name="c1")
|
||||||
|
doc_type = DocumentType.objects.create(name="dt1")
|
||||||
|
storage_path = StoragePath.objects.create(name="sp1")
|
||||||
|
tag = Tag.objects.create(name="tag")
|
||||||
|
|
||||||
|
matching_doc = Document.objects.create(
|
||||||
|
title="bank statement",
|
||||||
|
content="bank content",
|
||||||
|
checksum="A",
|
||||||
|
correspondent=correspondent,
|
||||||
|
document_type=doc_type,
|
||||||
|
storage_path=storage_path,
|
||||||
|
)
|
||||||
|
matching_doc.tags.add(tag)
|
||||||
|
|
||||||
|
with AsyncWriter(index.open_index()) as writer:
|
||||||
|
index.update_document(writer, matching_doc)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/documents/?query=bank&include_selection_data=true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("selection_data", response.data)
|
||||||
|
|
||||||
|
selected_correspondent = next(
|
||||||
|
item
|
||||||
|
for item in response.data["selection_data"]["selected_correspondents"]
|
||||||
|
if item["id"] == correspondent.id
|
||||||
|
)
|
||||||
|
selected_tag = next(
|
||||||
|
item
|
||||||
|
for item in response.data["selection_data"]["selected_tags"]
|
||||||
|
if item["id"] == tag.id
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(selected_correspondent["document_count"], 1)
|
||||||
|
self.assertEqual(selected_tag["document_count"], 1)
|
||||||
|
|
||||||
def test_search_custom_field_ordering(self) -> None:
|
def test_search_custom_field_ordering(self) -> None:
|
||||||
custom_field = CustomField.objects.create(
|
custom_field = CustomField.objects.create(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ SIDEBAR_VIEWS_VISIBLE_IDS_KEY = "sidebar_views_visible_ids"
|
|||||||
|
|
||||||
|
|
||||||
class TestMigrateSavedViewVisibilityToUiSettings(TestMigrations):
|
class TestMigrateSavedViewVisibilityToUiSettings(TestMigrations):
|
||||||
migrate_from = "0013_document_root_document"
|
migrate_from = "0013_alter_paperlesstask_task_name"
|
||||||
migrate_to = "0015_savedview_visibility_to_ui_settings"
|
migrate_to = "0014_savedview_visibility_to_ui_settings"
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps) -> None:
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
User = apps.get_model("auth", "User")
|
User = apps.get_model("auth", "User")
|
||||||
@@ -132,8 +132,8 @@ class TestMigrateSavedViewVisibilityToUiSettings(TestMigrations):
|
|||||||
|
|
||||||
|
|
||||||
class TestReverseMigrateSavedViewVisibilityFromUiSettings(TestMigrations):
|
class TestReverseMigrateSavedViewVisibilityFromUiSettings(TestMigrations):
|
||||||
migrate_from = "0015_savedview_visibility_to_ui_settings"
|
migrate_from = "0014_savedview_visibility_to_ui_settings"
|
||||||
migrate_to = "0014_alter_paperlesstask_task_name"
|
migrate_to = "0013_alter_paperlesstask_task_name"
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps) -> None:
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
User = apps.get_model("auth", "User")
|
User = apps.get_model("auth", "User")
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from documents.tests.utils import TestMigrations
|
|||||||
|
|
||||||
|
|
||||||
class TestMigrateShareLinkBundlePermissions(TestMigrations):
|
class TestMigrateShareLinkBundlePermissions(TestMigrations):
|
||||||
migrate_from = "0007_document_content_length"
|
migrate_from = "0006_document_content_length"
|
||||||
migrate_to = "0008_sharelinkbundle"
|
migrate_to = "0007_sharelinkbundle"
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps) -> None:
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
User = apps.get_model("auth", "User")
|
User = apps.get_model("auth", "User")
|
||||||
@@ -24,8 +24,8 @@ class TestMigrateShareLinkBundlePermissions(TestMigrations):
|
|||||||
|
|
||||||
|
|
||||||
class TestReverseMigrateShareLinkBundlePermissions(TestMigrations):
|
class TestReverseMigrateShareLinkBundlePermissions(TestMigrations):
|
||||||
migrate_from = "0008_sharelinkbundle"
|
migrate_from = "0007_sharelinkbundle"
|
||||||
migrate_to = "0007_document_content_length"
|
migrate_to = "0006_document_content_length"
|
||||||
|
|
||||||
def setUpBeforeMigration(self, apps) -> None:
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
User = apps.get_model("auth", "User")
|
User = apps.get_model("auth", "User")
|
||||||
|
|||||||
@@ -555,7 +555,9 @@ class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet):
|
|||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
response = self.get_paginated_response(serializer.data)
|
response = self.get_paginated_response(serializer.data)
|
||||||
if descendant_pks:
|
response.data["display_count"] = len(children_source)
|
||||||
|
api_version = int(request.version or settings.REST_FRAMEWORK["DEFAULT_VERSION"])
|
||||||
|
if descendant_pks and api_version < 10:
|
||||||
# Include children in the "all" field, if needed
|
# Include children in the "all" field, if needed
|
||||||
response.data["all"] = [tag.pk for tag in children_source]
|
response.data["all"] = [tag.pk for tag in children_source]
|
||||||
return response
|
return response
|
||||||
@@ -835,6 +837,61 @@ class DocumentViewSet(
|
|||||||
"custom_field_",
|
"custom_field_",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_selection_data_for_queryset(self, queryset):
|
||||||
|
correspondents = Correspondent.objects.annotate(
|
||||||
|
document_count=Count(
|
||||||
|
"documents",
|
||||||
|
filter=Q(documents__in=queryset),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
tags = Tag.objects.annotate(
|
||||||
|
document_count=Count(
|
||||||
|
"documents",
|
||||||
|
filter=Q(documents__in=queryset),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
document_types = DocumentType.objects.annotate(
|
||||||
|
document_count=Count(
|
||||||
|
"documents",
|
||||||
|
filter=Q(documents__in=queryset),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
storage_paths = StoragePath.objects.annotate(
|
||||||
|
document_count=Count(
|
||||||
|
"documents",
|
||||||
|
filter=Q(documents__in=queryset),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
custom_fields = CustomField.objects.annotate(
|
||||||
|
document_count=Count(
|
||||||
|
"fields__document",
|
||||||
|
filter=Q(fields__document__in=queryset),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"selected_correspondents": [
|
||||||
|
{"id": t.id, "document_count": t.document_count} for t in correspondents
|
||||||
|
],
|
||||||
|
"selected_tags": [
|
||||||
|
{"id": t.id, "document_count": t.document_count} for t in tags
|
||||||
|
],
|
||||||
|
"selected_document_types": [
|
||||||
|
{"id": t.id, "document_count": t.document_count} for t in document_types
|
||||||
|
],
|
||||||
|
"selected_storage_paths": [
|
||||||
|
{"id": t.id, "document_count": t.document_count} for t in storage_paths
|
||||||
|
],
|
||||||
|
"selected_custom_fields": [
|
||||||
|
{"id": t.id, "document_count": t.document_count} for t in custom_fields
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
latest_version_content = Subquery(
|
latest_version_content = Subquery(
|
||||||
Document.objects.filter(root_document=OuterRef("pk"))
|
Document.objects.filter(root_document=OuterRef("pk"))
|
||||||
@@ -982,6 +1039,25 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
if not get_boolean(
|
||||||
|
str(request.query_params.get("include_selection_data", "false")),
|
||||||
|
):
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
selection_data = self._get_selection_data_for_queryset(queryset)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
response = self.get_paginated_response(serializer.data)
|
||||||
|
response.data["selection_data"] = selection_data
|
||||||
|
return response
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response({"results": serializer.data, "selection_data": selection_data})
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
from documents import index
|
from documents import index
|
||||||
|
|
||||||
@@ -1998,6 +2074,21 @@ class UnifiedSearchViewSet(DocumentViewSet):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if get_boolean(
|
||||||
|
str(
|
||||||
|
request.query_params.get(
|
||||||
|
"include_selection_data",
|
||||||
|
"false",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result_ids = queryset.get_result_ids()
|
||||||
|
response.data["selection_data"] = (
|
||||||
|
self._get_selection_data_for_queryset(
|
||||||
|
Document.objects.filter(pk__in=result_ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except NotFound:
|
except NotFound:
|
||||||
raise
|
raise
|
||||||
@@ -3786,20 +3877,55 @@ class BulkEditObjectsView(PassUserMixin):
|
|||||||
user = self.request.user
|
user = self.request.user
|
||||||
object_type = serializer.validated_data.get("object_type")
|
object_type = serializer.validated_data.get("object_type")
|
||||||
object_ids = serializer.validated_data.get("objects")
|
object_ids = serializer.validated_data.get("objects")
|
||||||
|
apply_to_all = serializer.validated_data.get("all")
|
||||||
object_class = serializer.get_object_class(object_type)
|
object_class = serializer.get_object_class(object_type)
|
||||||
operation = serializer.validated_data.get("operation")
|
operation = serializer.validated_data.get("operation")
|
||||||
|
model_name = object_class._meta.model_name
|
||||||
|
perm_codename = (
|
||||||
|
f"change_{model_name}"
|
||||||
|
if operation == "set_permissions"
|
||||||
|
else f"delete_{model_name}"
|
||||||
|
)
|
||||||
|
|
||||||
objs = object_class.objects.select_related("owner").filter(pk__in=object_ids)
|
if apply_to_all:
|
||||||
|
# Support all to avoid sending large lists of ids for bulk operations, with optional filters
|
||||||
|
filters = serializer.validated_data.get("filters") or {}
|
||||||
|
filterset_class = {
|
||||||
|
"tags": TagFilterSet,
|
||||||
|
"correspondents": CorrespondentFilterSet,
|
||||||
|
"document_types": DocumentTypeFilterSet,
|
||||||
|
"storage_paths": StoragePathFilterSet,
|
||||||
|
}[object_type]
|
||||||
|
user_permitted_objects = get_objects_for_user_owner_aware(
|
||||||
|
user,
|
||||||
|
perm_codename,
|
||||||
|
object_class,
|
||||||
|
)
|
||||||
|
objs = filterset_class(
|
||||||
|
data=filters,
|
||||||
|
queryset=user_permitted_objects,
|
||||||
|
).qs
|
||||||
|
if object_type == "tags":
|
||||||
|
editable_ids = set(user_permitted_objects.values_list("pk", flat=True))
|
||||||
|
all_ids = set(objs.values_list("pk", flat=True))
|
||||||
|
for tag in objs:
|
||||||
|
all_ids.update(
|
||||||
|
descendant.pk
|
||||||
|
for descendant in tag.get_descendants()
|
||||||
|
if descendant.pk in editable_ids
|
||||||
|
)
|
||||||
|
objs = object_class.objects.filter(pk__in=all_ids)
|
||||||
|
objs = objs.select_related("owner")
|
||||||
|
object_ids = list(objs.values_list("pk", flat=True))
|
||||||
|
else:
|
||||||
|
objs = object_class.objects.select_related("owner").filter(
|
||||||
|
pk__in=object_ids,
|
||||||
|
)
|
||||||
|
|
||||||
if not user.is_superuser:
|
if not user.is_superuser:
|
||||||
model_name = object_class._meta.model_name
|
perm = f"documents.{perm_codename}"
|
||||||
perm = (
|
|
||||||
f"documents.change_{model_name}"
|
|
||||||
if operation == "set_permissions"
|
|
||||||
else f"documents.delete_{model_name}"
|
|
||||||
)
|
|
||||||
has_perms = user.has_perm(perm) and all(
|
has_perms = user.has_perm(perm) and all(
|
||||||
(obj.owner == user or obj.owner is None) for obj in objs
|
has_perms_owner_aware(user, perm_codename, obj) for obj in objs
|
||||||
)
|
)
|
||||||
|
|
||||||
if not has_perms:
|
if not has_perms:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.checks import Error
|
from django.core.checks import Error
|
||||||
|
from django.core.checks import Tags
|
||||||
from django.core.checks import Warning
|
from django.core.checks import Warning
|
||||||
from django.core.checks import register
|
from django.core.checks import register
|
||||||
from django.db import connections
|
from django.db import connections
|
||||||
@@ -204,15 +205,16 @@ def audit_log_check(app_configs, **kwargs):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@register()
|
@register(Tags.compatibility)
|
||||||
def check_v3_minimum_upgrade_version(
|
def check_v3_minimum_upgrade_version(
|
||||||
app_configs: object,
|
app_configs: object,
|
||||||
**kwargs: object,
|
**kwargs: object,
|
||||||
) -> list[Error]:
|
) -> list[Error]:
|
||||||
"""Enforce that upgrades to v3 must start from v2.20.9.
|
"""
|
||||||
|
Enforce that upgrades to v3 must start from v2.20.10.
|
||||||
|
|
||||||
v3 squashes all prior migrations into 0001_squashed and 0002_squashed.
|
v3 squashes all prior migrations into 0001_squashed and 0002_squashed.
|
||||||
If a user skips v2.20.9, the data migration in 1075_workflowaction_order
|
If a user skips v2.20.10, the data migration in 1075_workflowaction_order
|
||||||
never runs and the squash may apply schema changes against an incomplete
|
never runs and the squash may apply schema changes against an incomplete
|
||||||
database state.
|
database state.
|
||||||
"""
|
"""
|
||||||
@@ -239,7 +241,7 @@ def check_v3_minimum_upgrade_version(
|
|||||||
if {"0001_squashed", "0002_squashed"} & applied:
|
if {"0001_squashed", "0002_squashed"} & applied:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# On v2.20.9 exactly — squash will pick up cleanly from here
|
# On v2.20.10 exactly — squash will pick up cleanly from here
|
||||||
if "1075_workflowaction_order" in applied:
|
if "1075_workflowaction_order" in applied:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -250,8 +252,8 @@ def check_v3_minimum_upgrade_version(
|
|||||||
Error(
|
Error(
|
||||||
"Cannot upgrade to Paperless-ngx v3 from this version.",
|
"Cannot upgrade to Paperless-ngx v3 from this version.",
|
||||||
hint=(
|
hint=(
|
||||||
"Upgrading to v3 can only be performed from v2.20.9."
|
"Upgrading to v3 can only be performed from v2.20.10."
|
||||||
"Please upgrade to v2.20.9, run migrations, then upgrade to v3."
|
"Please upgrade to v2.20.10, run migrations, then upgrade to v3."
|
||||||
"See https://docs.paperless-ngx.com/setup/#upgrading for details."
|
"See https://docs.paperless-ngx.com/setup/#upgrading for details."
|
||||||
),
|
),
|
||||||
id="paperless.E002",
|
id="paperless.E002",
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ def parse_db_settings(data_dir: Path) -> dict[str, dict[str, Any]]:
|
|||||||
engine = get_choice_from_env(
|
engine = get_choice_from_env(
|
||||||
"PAPERLESS_DBENGINE",
|
"PAPERLESS_DBENGINE",
|
||||||
{"sqlite", "postgresql", "mariadb"},
|
{"sqlite", "postgresql", "mariadb"},
|
||||||
default="sqlite",
|
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# MariaDB users already had to set PAPERLESS_DBENGINE, so it was picked up above
|
# MariaDB users already had to set PAPERLESS_DBENGINE, so it was picked up above
|
||||||
|
|||||||
@@ -581,11 +581,11 @@ class TestV3MinimumUpgradeVersionCheck:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- DB is on an old v2 version (pre-v2.20.9)
|
- DB is on an old v2 version (pre-v2.20.10)
|
||||||
WHEN:
|
WHEN:
|
||||||
- The v3 upgrade check runs
|
- The v3 upgrade check runs
|
||||||
THEN:
|
THEN:
|
||||||
- The error hint explicitly references v2.20.9 so users know what to do
|
- The error hint explicitly references v2.20.10 so users know what to do
|
||||||
"""
|
"""
|
||||||
mocker.patch.dict(
|
mocker.patch.dict(
|
||||||
"paperless.checks.connections",
|
"paperless.checks.connections",
|
||||||
@@ -593,7 +593,7 @@ class TestV3MinimumUpgradeVersionCheck:
|
|||||||
)
|
)
|
||||||
result = check_v3_minimum_upgrade_version(None)
|
result = check_v3_minimum_upgrade_version(None)
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert "v2.20.9" in result[0].hint
|
assert "v2.20.10" in result[0].hint
|
||||||
|
|
||||||
def test_db_error_is_swallowed(self, mocker: MockerFixture) -> None:
|
def test_db_error_is_swallowed(self, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from allauth.mfa.recovery_codes.internal.flows import auto_generate_recovery_cod
|
|||||||
from allauth.mfa.totp.internal import auth as totp_auth
|
from allauth.mfa.totp.internal import auth as totp_auth
|
||||||
from allauth.socialaccount.adapter import get_adapter
|
from allauth.socialaccount.adapter import get_adapter
|
||||||
from allauth.socialaccount.models import SocialAccount
|
from allauth.socialaccount.models import SocialAccount
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
@@ -56,17 +57,27 @@ class StandardPagination(PageNumberPagination):
|
|||||||
page_size_query_param = "page_size"
|
page_size_query_param = "page_size"
|
||||||
max_page_size = 100000
|
max_page_size = 100000
|
||||||
|
|
||||||
|
def _get_api_version(self) -> int:
|
||||||
|
request = getattr(self, "request", None)
|
||||||
|
default_version = settings.REST_FRAMEWORK["DEFAULT_VERSION"]
|
||||||
|
return int(request.version if request else default_version)
|
||||||
|
|
||||||
|
def _should_include_all(self) -> bool:
|
||||||
|
# TODO: remove legacy `all` support when API v9 is dropped.
|
||||||
|
return self._get_api_version() < 10
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
|
response_data = [
|
||||||
|
("count", self.page.paginator.count),
|
||||||
|
("next", self.get_next_link()),
|
||||||
|
("previous", self.get_previous_link()),
|
||||||
|
]
|
||||||
|
if self._should_include_all():
|
||||||
|
response_data.append(("all", self.get_all_result_ids()))
|
||||||
|
response_data.append(("results", data))
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
OrderedDict(
|
OrderedDict(response_data),
|
||||||
[
|
|
||||||
("count", self.page.paginator.count),
|
|
||||||
("next", self.get_next_link()),
|
|
||||||
("previous", self.get_previous_link()),
|
|
||||||
("all", self.get_all_result_ids()),
|
|
||||||
("results", data),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_all_result_ids(self):
|
def get_all_result_ids(self):
|
||||||
@@ -87,11 +98,14 @@ class StandardPagination(PageNumberPagination):
|
|||||||
|
|
||||||
def get_paginated_response_schema(self, schema):
|
def get_paginated_response_schema(self, schema):
|
||||||
response_schema = super().get_paginated_response_schema(schema)
|
response_schema = super().get_paginated_response_schema(schema)
|
||||||
response_schema["properties"]["all"] = {
|
if self._should_include_all():
|
||||||
"type": "array",
|
response_schema["properties"]["all"] = {
|
||||||
"example": "[1, 2, 3]",
|
"type": "array",
|
||||||
"items": {"type": "integer"},
|
"example": "[1, 2, 3]",
|
||||||
}
|
"items": {"type": "integer"},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response_schema["properties"].pop("all", None)
|
||||||
return response_schema
|
return response_schema
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user