Compare commits

..
Author SHA1 Message Date
Crowdin Bot 7be7bdb467 New Crowdin translations by GitHub Action 2026-07-21 00:49:55 +00:00
117 changed files with 7076 additions and 7303 deletions
-4
View File
@@ -1,6 +1,5 @@
import { expect, test } from '@playwright/test'
import path from 'node:path'
import { mockFilterSelectionData } from '../mock-filter-selection-data'
const REQUESTS_HAR = path.join(__dirname, 'requests/api-settings.har')
@@ -8,7 +7,6 @@ test('should activate / deactivate save button when settings change', async ({
page,
}) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/settings')
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
await page.getByLabel('Use system setting').click()
@@ -18,7 +16,6 @@ test('should activate / deactivate save button when settings change', async ({
test('should warn on unsaved changes', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/settings')
await page.getByLabel('Use system setting').click()
await page.getByRole('link', { name: 'Dashboard' }).click()
@@ -31,7 +28,6 @@ test('should warn on unsaved changes', async ({ page }) => {
test('should apply appearance changes when set', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/settings')
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /auto/)
await page.getByLabel('Use system setting').click()
-6
View File
@@ -1,6 +1,5 @@
import { expect, test } from '@playwright/test'
import path from 'node:path'
import { mockFilterSelectionData } from '../mock-filter-selection-data'
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-dashboard1.har')
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-dashboard2.har')
@@ -9,7 +8,6 @@ const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-dashboard4.har')
test('dashboard inbox link', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await page.getByRole('link', { name: 'Documents in inbox' }).click()
await expect(page).toHaveURL(/tags__id__in=9/)
@@ -18,7 +16,6 @@ test('dashboard inbox link', async ({ page }) => {
test('dashboard total documents link', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await page.getByRole('link').filter({ hasText: 'Total documents' }).click()
await expect(page).toHaveURL(/documents/)
@@ -28,7 +25,6 @@ test('dashboard total documents link', async ({ page }) => {
test('dashboard saved view show all', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await page
.locator('pngx-widget-frame')
@@ -42,7 +38,6 @@ test('dashboard saved view show all', async ({ page }) => {
test('dashboard saved view document links', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR4, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await page
.locator('pngx-widget-frame')
@@ -56,7 +51,6 @@ test('dashboard saved view document links', async ({ page }) => {
test('test slim sidebar', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await page.locator('.sidebar-slim-toggler').click()
await expect(
@@ -1,6 +1,5 @@
import { expect, test } from '@playwright/test'
import path from 'node:path'
import { mockFilterSelectionData } from '../mock-filter-selection-data'
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-detail2.har')
@@ -9,7 +8,6 @@ test('should activate / deactivate save button when changes are saved', async ({
page,
}) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents/175/')
await page.waitForSelector('pngx-document-detail pngx-input-text:first-child')
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
@@ -22,7 +20,6 @@ test('should activate / deactivate save button when changes are saved', async ({
test('should warn on unsaved changes', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents/175/')
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
/\w+/
@@ -42,7 +39,6 @@ test('should warn on unsaved changes', async ({ page }) => {
test('should support tab direct navigation', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents/175/details')
await expect(page.getByRole('tab', { name: 'Details' })).toHaveAttribute(
'aria-selected',
@@ -72,7 +68,6 @@ test('should support tab direct navigation', async ({ page }) => {
test('should show a mobile preview', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents/175/')
await page.setViewportSize({ width: 400, height: 1000 })
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
@@ -82,7 +77,6 @@ test('should show a mobile preview', async ({ page }) => {
test('should show a list of notes', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents/175/notes')
await expect(page.locator('pngx-document-notes')).toBeVisible()
await expect(
@@ -95,7 +89,6 @@ test('should show a list of notes', async ({ page }) => {
test('should support quick filters', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents/175/details')
await page
.getByRole('button', { name: 'Filter documents with these Tags' })
@@ -1,6 +1,5 @@
import { expect, test } from '@playwright/test'
import path from 'node:path'
import { mockFilterSelectionData } from '../mock-filter-selection-data'
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-document-list1.har')
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-list2.har')
@@ -11,7 +10,6 @@ const REQUESTS_HAR6 = path.join(__dirname, 'requests/api-document-list6.har')
test('basic filtering', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents')
await page.getByRole('button', { name: 'Tags' }).click()
await page.getByRole('menuitem', { name: 'Inbox' }).click()
@@ -47,7 +45,6 @@ test('basic filtering', async ({ page }) => {
test('text filtering', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents')
await page.getByRole('main').getByRole('combobox').click()
await page.getByRole('main').getByRole('combobox').fill('test')
@@ -84,7 +81,6 @@ test('text filtering', async ({ page }) => {
test('date filtering', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents')
await page.getByRole('button', { name: 'Dates' }).click()
await page.locator('.ng-arrow-wrapper').first().click()
@@ -107,7 +103,6 @@ test('date filtering', async ({ page }) => {
test('sorting', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR4, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents')
await page.getByRole('button', { name: 'Sort' }).click()
await page.getByRole('button', { name: 'ASN' }).click()
@@ -146,7 +141,6 @@ test('sorting', async ({ page }) => {
test('change views', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents')
await page.locator('.btn-group > label').first().click()
await expect(page.locator('pngx-document-list table')).toBeVisible()
@@ -158,7 +152,6 @@ test('change views', async ({ page }) => {
test('bulk edit', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR6, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/documents')
await page.locator('pngx-document-card-small').nth(0).click()
-35
View File
@@ -1,35 +0,0 @@
import { Page } from '@playwright/test'
const EMPTY_SELECTION_DATA = {
selected_correspondents: [],
selected_tags: [],
selected_document_types: [],
selected_storage_paths: [],
selected_custom_fields: [],
}
/**
* The document list now fires a GET to filter_selection_data on every
* non-search reload(), independent of and concurrent with the main list
* request. It's not present in any of the recorded HAR fixtures, so with
* `notFound: 'fallback'` it would otherwise fall through to the real
* network (nothing listens there in e2e, since only the frontend dev
* server is started) and fail every test that reloads the list.
*
* Playwright checks routes in reverse-registration order, so this must be
* registered before a test's own page.routeFromHAR() call for the HAR
* route's `notFound: 'fallback'` to defer back to this one.
*/
export async function mockFilterSelectionData(page: Page) {
await page.route('**/api/documents/filter_selection_data/**', (route) =>
route.fulfill({
json: EMPTY_SELECTION_DATA,
// The app calls the (cross-origin, from the e2e app's perspective)
// backend at http://localhost:8000 while served from :4200, so a
// fulfilled response needs the same CORS header the real backend
// sends (and that recorded HAR responses already carry) or the
// browser rejects it as a cross-origin failure.
headers: { 'Access-Control-Allow-Origin': 'http://localhost:4200' },
})
)
}
@@ -1,12 +1,10 @@
import { expect, test } from '@playwright/test'
import path from 'node:path'
import { mockFilterSelectionData } from '../mock-filter-selection-data'
const REQUESTS_HAR = path.join(__dirname, 'requests/api-global-permissions.har')
test('should not allow user to edit settings', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
await page.goto('/settings')
@@ -17,7 +15,6 @@ test('should not allow user to edit settings', async ({ page }) => {
test('should not allow user to view documents', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(
page.locator('nav').getByRole('link', { name: 'Documents' })
@@ -34,7 +31,6 @@ test('should not allow user to view documents', async ({ page }) => {
test('should not allow user to view correspondents', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
@@ -47,7 +43,6 @@ test('should not allow user to view correspondents', async ({ page }) => {
test('should not allow user to view tags', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
@@ -60,7 +55,6 @@ test('should not allow user to view tags', async ({ page }) => {
test('should not allow user to view document types', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
@@ -73,7 +67,6 @@ test('should not allow user to view document types', async ({ page }) => {
test('should not allow user to view storage paths', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
@@ -86,7 +79,6 @@ test('should not allow user to view storage paths', async ({ page }) => {
test('should not allow user to view logs', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
await page.goto('/logs')
@@ -97,7 +89,6 @@ test('should not allow user to view logs', async ({ page }) => {
test('should not allow user to view tasks', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await mockFilterSelectionData(page)
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
await page.goto('/tasks')
@@ -191,14 +191,6 @@ describe('BulkEditorComponent', () => {
})
afterEach(async () => {
// A filter_selection_data request now fires concurrently with every
// non-search reload(), independent of whether a given test flushes or
// even inspects the primary list response. Drain any left unclaimed.
httpTestingController.match(
(request) =>
request.url ===
`${environment.apiBaseUrl}documents/filter_selection_data/`
)
httpTestingController.verify()
})
@@ -394,7 +386,7 @@ describe('BulkEditorComponent', () => {
parameters: { add_tags: [101], remove_tags: [] },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -440,7 +432,7 @@ describe('BulkEditorComponent', () => {
parameters: { add_tags: [101], remove_tags: [] },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
})
@@ -469,7 +461,7 @@ describe('BulkEditorComponent', () => {
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
.flush(true)
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -560,7 +552,7 @@ describe('BulkEditorComponent', () => {
parameters: { correspondent: 101 },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -592,7 +584,7 @@ describe('BulkEditorComponent', () => {
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
.flush(true)
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -658,7 +650,7 @@ describe('BulkEditorComponent', () => {
parameters: { document_type: 101 },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -690,7 +682,7 @@ describe('BulkEditorComponent', () => {
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
.flush(true)
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -756,7 +748,7 @@ describe('BulkEditorComponent', () => {
parameters: { storage_path: 101 },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -788,7 +780,7 @@ describe('BulkEditorComponent', () => {
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
.flush(true)
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -854,7 +846,7 @@ describe('BulkEditorComponent', () => {
parameters: { add_custom_fields: [101], remove_custom_fields: [102] },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -886,7 +878,7 @@ describe('BulkEditorComponent', () => {
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
.flush(true)
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -995,7 +987,7 @@ describe('BulkEditorComponent', () => {
documents: [3, 4],
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1088,7 +1080,7 @@ describe('BulkEditorComponent', () => {
documents: [3, 4],
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1123,7 +1115,7 @@ describe('BulkEditorComponent', () => {
source_mode: 'latest_version',
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1164,7 +1156,7 @@ describe('BulkEditorComponent', () => {
metadata_document_id: 3,
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1183,7 +1175,7 @@ describe('BulkEditorComponent', () => {
delete_originals: true,
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1204,7 +1196,7 @@ describe('BulkEditorComponent', () => {
archive_fallback: true,
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1307,7 +1299,7 @@ describe('BulkEditorComponent', () => {
},
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -1615,7 +1607,7 @@ describe('BulkEditorComponent', () => {
expect(toastServiceShowInfoSpy).toHaveBeenCalled()
expect(listReloadSpy).toHaveBeenCalled()
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
@@ -84,28 +84,6 @@ const view: SavedView = {
filter_rules: filterRules,
}
const emptySelectionData = {
selected_correspondents: [],
selected_tags: [],
selected_document_types: [],
selected_storage_paths: [],
selected_custom_fields: [],
}
// A successful (non-search) list response now triggers a separate,
// non-blocking request for filter dropdown counts. Tests that flush a
// successful list response need to also flush this follow-up request.
function flushSelectionDataRequest(
httpTestingController: HttpTestingController,
querySuffix: string = ''
) {
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/filter_selection_data/${querySuffix}`
)
expect(req.request.method).toEqual('GET')
req.flush(emptySelectionData)
}
describe('DocumentListViewService', () => {
let httpTestingController: HttpTestingController
let documentListViewService: DocumentListViewService
@@ -127,7 +105,6 @@ describe('DocumentListViewService', () => {
})
sessionStorage.clear()
localStorage.clear()
httpTestingController = TestBed.inject(HttpTestingController)
documentListViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
@@ -137,19 +114,8 @@ describe('DocumentListViewService', () => {
afterEach(() => {
documentListViewService.cancelPending()
// A filter_selection_data request now fires concurrently with every
// non-search reload(), independent of whether the test cares about or
// flushes the primary list response. Drain any that a test didn't
// explicitly claim via flushSelectionDataRequest, so unrelated tests
// don't have to know about this follow-up request to pass verify().
httpTestingController.match(
(request) =>
request.url ===
`${environment.apiBaseUrl}documents/filter_selection_data/`
)
httpTestingController.verify()
sessionStorage.clear()
localStorage.clear()
})
afterAll(() => {
@@ -162,11 +128,10 @@ describe('DocumentListViewService', () => {
expect(documentListViewService.currentPage).toEqual(1)
documentListViewService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
expect(req.request.method).toEqual('GET')
expect(documentListViewService.isReloading).toBeFalsy()
expect(documentListViewService.activeSavedViewId).toBeNull()
@@ -178,12 +143,12 @@ describe('DocumentListViewService', () => {
it('should handle error on page request out of range', () => {
documentListViewService.currentPage = 50
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=50&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=50&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush([], { status: 404, statusText: 'Unexpected error' })
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
expect(documentListViewService.currentPage).toEqual(1)
@@ -200,20 +165,21 @@ describe('DocumentListViewService', () => {
]
documentListViewService.setFilterRules(filterRulesAny)
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false&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')
req.flush(
{ archive_serial_number: 'hello' },
{ status: 404, statusText: 'Unexpected error' }
)
// the error is a plain field error (not a page-out-of-range or deleted
// custom-field-sort case), so no automatic retry request is sent here
expect(documentListViewService.error).toBeTruthy()
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
// reset the list
documentListViewService.setFilterRules([])
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
})
@@ -221,7 +187,7 @@ describe('DocumentListViewService', () => {
documentListViewService.currentPage = 1
documentListViewService.sortField = 'custom_field_999'
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-custom_field_999&truncate_content=true&include_selection_data=false`
`${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')
req.flush(
@@ -230,7 +196,7 @@ describe('DocumentListViewService', () => {
)
// resets itself
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
})
@@ -245,7 +211,7 @@ describe('DocumentListViewService', () => {
]
documentListViewService.setFilterRules(filterRulesAny)
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false&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')
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
@@ -253,7 +219,7 @@ describe('DocumentListViewService', () => {
// reset the list
documentListViewService.setFilterRules([])
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
})
@@ -262,7 +228,7 @@ describe('DocumentListViewService', () => {
expect(documentListViewService.sortReverse).toBeTruthy()
documentListViewService.setSort('added', false)
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=added&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=added&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
expect(documentListViewService.sortField).toEqual('added')
@@ -270,12 +236,12 @@ describe('DocumentListViewService', () => {
documentListViewService.sortField = 'created'
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=created&truncate_content=true&include_selection_data=true`
)
expect(documentListViewService.sortField).toEqual('created')
documentListViewService.sortReverse = true
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
expect(documentListViewService.sortReverse).toBeTruthy()
@@ -318,7 +284,7 @@ describe('DocumentListViewService', () => {
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
documentListViewService.pageSize
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true&include_selection_data=false`
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
expect(documentListViewService.currentPage).toEqual(page)
@@ -335,7 +301,7 @@ describe('DocumentListViewService', () => {
}
documentListViewService.loadFromQueryParams(convertToParamMap(params))
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&include_selection_data=false&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(documentListViewService.filterRules).toEqual([
@@ -345,16 +311,12 @@ describe('DocumentListViewService', () => {
},
])
req.flush(full_results)
flushSelectionDataRequest(
httpTestingController,
`?tags__id__all=${tags__id__all}`
)
})
it('should use filter rules to update query params', () => {
documentListViewService.setFilterRules(filterRules)
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&include_selection_data=false&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')
})
@@ -363,31 +325,26 @@ describe('DocumentListViewService', () => {
documentListViewService.currentPage = 2
let req = httpTestingController.expectOne((request) =>
request.urlWithParams.startsWith(
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
)
expect(req.request.method).toEqual('GET')
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.setFilterRules(filterRules, true)
const filteredReqs = httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false&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)
filteredReqs[0].flush(full_results)
flushSelectionDataRequest(
httpTestingController,
`?tags__id__all=${tags__id__all}`
)
expect(documentListViewService.currentPage).toEqual(1)
})
it('should support quick filter', () => {
documentListViewService.quickFilter(filterRules)
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&include_selection_data=false&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')
})
@@ -410,21 +367,21 @@ describe('DocumentListViewService', () => {
convertToParamMap(params)
)
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&include_selection_data=false&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')
// reset the list
documentListViewService.currentPage = 1
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&include_selection_data=false&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([])
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&include_selection_data=true`
)
documentListViewService.sortField = 'created'
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
documentListViewService.activateSavedView(null)
})
@@ -432,22 +389,18 @@ describe('DocumentListViewService', () => {
it('should support navigating next / previous', () => {
documentListViewService.setFilterRules([])
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(documentListViewService.currentPage).toEqual(1)
documentListViewService.pageSize = 3
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush({
count: 3,
results: documents.slice(0, 3),
})
// two reload()s ran above (setFilterRules, then pageSize), each firing
// its own concurrent filter_selection_data request with an identical
// (unfiltered) URL; this test doesn't assert on selectionData, so let
// afterEach's drain step clean both up rather than disambiguating here.
expect(documentListViewService.hasNext(documents[0].id)).toBeTruthy()
expect(documentListViewService.hasPrevious(documents[0].id)).toBeFalsy()
documentListViewService.getNext(documents[0].id).subscribe((docId) => {
@@ -494,7 +447,7 @@ describe('DocumentListViewService', () => {
expect(documentListViewService.currentPage).toEqual(1)
documentListViewService.pageSize = 3
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
)
jest
.spyOn(documentListViewService, 'getLastPage')
@@ -509,7 +462,7 @@ describe('DocumentListViewService', () => {
expect(reloadSpy).toHaveBeenCalled()
expect(documentListViewService.currentPage).toEqual(2)
const reqs = httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(reqs.length).toBeGreaterThan(0)
})
@@ -544,11 +497,11 @@ describe('DocumentListViewService', () => {
.mockReturnValue(documents)
documentListViewService.currentPage = 2
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
documentListViewService.pageSize = 3
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
)
const reloadSpy = jest.spyOn(documentListViewService, 'reload')
documentListViewService.getPrevious(1).subscribe({
@@ -558,7 +511,7 @@ describe('DocumentListViewService', () => {
expect(reloadSpy).toHaveBeenCalled()
expect(documentListViewService.currentPage).toEqual(1)
const reqs = httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(reqs.length).toBeGreaterThan(0)
})
@@ -571,11 +524,10 @@ describe('DocumentListViewService', () => {
it('should support select a document', () => {
documentListViewService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.toggleSelected(documents[0])
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
documentListViewService.toggleSelected(documents[0])
@@ -585,11 +537,10 @@ describe('DocumentListViewService', () => {
it('should support select all', () => {
documentListViewService.reload()
const reloadReq = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(reloadReq.request.method).toEqual('GET')
reloadReq.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.selectAll()
expect(documentListViewService.allSelected).toBeTruthy()
@@ -602,14 +553,13 @@ describe('DocumentListViewService', () => {
it('should support select page', () => {
documentListViewService.pageSize = 3
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush({
count: 3,
results: documents.slice(0, 3),
})
flushSelectionDataRequest(httpTestingController)
documentListViewService.selectPage()
expect(documentListViewService.selected.size).toEqual(3)
expect(documentListViewService.isSelected(documents[5])).toBeFalsy()
@@ -618,11 +568,10 @@ describe('DocumentListViewService', () => {
it('should support select range', () => {
documentListViewService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.toggleSelected(documents[0])
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
documentListViewService.selectRangeTo(documents[2])
@@ -634,10 +583,9 @@ describe('DocumentListViewService', () => {
it('should clear all-selected mode when toggling a single document', () => {
documentListViewService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.selectAll()
expect(documentListViewService.allSelected).toBeTruthy()
@@ -651,10 +599,9 @@ describe('DocumentListViewService', () => {
it('should clear all-selected mode when selecting a range', () => {
documentListViewService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.selectAll()
documentListViewService.toggleSelected(documents[1])
@@ -672,24 +619,22 @@ describe('DocumentListViewService', () => {
it('should support selection range reduction', () => {
documentListViewService.reload()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush(full_results)
flushSelectionDataRequest(httpTestingController)
documentListViewService.selectAll()
expect(documentListViewService.selected.size).toEqual(6)
documentListViewService.setFilterRules(filterRules)
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false&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`
)
req.flush({
count: 3,
results: documents.slice(0, 3),
})
flushSelectionDataRequest(httpTestingController, '?tags__id__all=9')
expect(documentListViewService.allSelected).toBeTruthy()
expect(documentListViewService.selected.size).toEqual(3)
})
@@ -698,7 +643,7 @@ describe('DocumentListViewService', () => {
const cancelSpy = jest.spyOn(documentListViewService, 'cancelPending')
documentListViewService.reload()
httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=9`
)
expect(cancelSpy).toHaveBeenCalled()
})
@@ -717,7 +662,7 @@ describe('DocumentListViewService', () => {
documentListViewService.setFilterRules([])
expect(documentListViewService.sortField).toEqual('created')
httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
})
@@ -744,11 +689,11 @@ describe('DocumentListViewService', () => {
expect(localStorageSpy).toHaveBeenCalled()
// reload triggered
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
documentListViewService.displayFields = null
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(documentListViewService.displayFields).toEqual(
DEFAULT_DISPLAY_FIELDS.filter((f) => f.id !== DisplayField.ADDED).map(
@@ -793,7 +738,7 @@ describe('DocumentListViewService', () => {
it('should generate quick filter URL preserving default state', () => {
documentListViewService.reload()
httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=false`
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
const urlTree = documentListViewService.getQuickFilterUrl(filterRules)
expect(urlTree).toBeDefined()
@@ -314,39 +314,12 @@ export class DocumentListViewService {
}
}
private loadFilterSelectionData(filterRules: FilterRule[]) {
this.documentService
.getFilterSelectionData(filterRules)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (selectionData) => {
this.selectionData = selectionData
this.markChanged()
},
error: () => {
this.selectionData = null
this.markChanged()
},
})
}
reload(onFinish?, updateQueryParams: boolean = true) {
this.cancelPending()
this.isReloading = true
this.error = null
this.markChanged()
let activeListViewState = this.activeListViewState
// Full-text search results are already narrowed by the search backend, so
// computing selection data inline there is cheap. A plain (unfiltered or
// ORM-filtered) browse can span the entire document set, so its selection
// data is fetched separately -- concurrently with the list itself, rather
// than blocking or waiting on it.
const isFullTextSearch = isFullTextFilterRule(
activeListViewState.filterRules
)
if (!isFullTextSearch) {
this.loadFilterSelectionData(activeListViewState.filterRules)
}
this.documentService
.listFiltered(
activeListViewState.currentPage,
@@ -354,22 +327,17 @@ export class DocumentListViewService {
activeListViewState.sortField,
activeListViewState.sortReverse,
activeListViewState.filterRules,
{
truncate_content: true,
include_selection_data: isFullTextSearch,
}
{ truncate_content: true, include_selection_data: true }
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
const resultWithSelectionData = result as DocumentResults
this.initialized = true
this.isReloading = false
activeListViewState.collectionSize = result.count
activeListViewState.documents = result.results
if (isFullTextSearch) {
this.selectionData =
(result as DocumentResults).selection_data ?? null
}
this.selectionData = resultWithSelectionData.selection_data ?? null
this.syncSelectedToCurrentPage()
this.markChanged()
@@ -408,9 +376,6 @@ export class DocumentListViewService {
// e.g. field was deleted
this.sortField = 'created'
} else {
// cancel the concurrently-fired selection-data request too, so it
// can't resolve afterward and clobber this reset with stale data
this.cancelPending()
this.selectionData = null
let errorMessage
if (
@@ -41,24 +41,6 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
}
}
/**
* Merges a plain params object into an HttpParams instance, skipping
* null/undefined values so they aren't serialized as literal "null" /
* "undefined" query string entries.
*/
protected withParams(
params,
base: HttpParams = new HttpParams()
): HttpParams {
let httpParams = base
for (let key in params) {
if (params[key] != null) {
httpParams = httpParams.set(key, params[key])
}
}
return httpParams
}
list(
page?: number,
pageSize?: number,
@@ -78,7 +60,11 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
if (ordering) {
httpParams = httpParams.set('ordering', ordering)
}
httpParams = this.withParams(extraParams, httpParams)
for (let extraParamKey in extraParams) {
if (extraParams[extraParamKey] != null) {
httpParams = httpParams.set(extraParamKey, extraParams[extraParamKey])
}
}
return this.http
.get<Results<T>>(this.getResourceUrl(), {
params: httpParams,
@@ -127,7 +113,11 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
httpParams = httpParams.set('id__in', ids.join(','))
httpParams = httpParams.set('ordering', '-id')
httpParams = httpParams.set('page_size', 1000)
httpParams = this.withParams(extraParams, httpParams)
for (let extraParamKey in extraParams) {
if (extraParams[extraParamKey] != null) {
httpParams = httpParams.set(extraParamKey, extraParams[extraParamKey])
}
}
return this.http
.get<Results<T>>(this.getResourceUrl(), {
params: httpParams,
@@ -398,13 +398,6 @@ export class DocumentService extends AbstractPaperlessService<Document> {
)
}
getFilterSelectionData(filterRules: FilterRule[]): Observable<SelectionData> {
return this.http.get<SelectionData>(
this.getResourceUrl(null, 'filter_selection_data'),
{ params: this.withParams(queryParamsFromFilterRules(filterRules)) }
)
}
getSuggestions(id: number): Observable<DocumentSuggestions> {
return this.http.get<DocumentSuggestions>(
this.getResourceUrl(id, 'suggestions')
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,9 +1,9 @@
from datetime import UTC
from datetime import datetime
from typing import Any
from django.conf import settings
from django.core.cache import cache
from rest_framework.request import Request
from documents.caching import CACHE_5_MINUTES
from documents.caching import CACHE_50_MINUTES
@@ -117,7 +117,7 @@ def preview_last_modified(request, pk: int) -> datetime | None:
return doc.modified
def thumbnail_etag(request: Request, pk: int) -> str | None:
def thumbnail_etag(request: Any, pk: int) -> str | None:
"""
Thumbnails are version-dependent, so use the effective document checksum as
the ETag to invalidate cache when the latest version changes.
@@ -128,7 +128,7 @@ def thumbnail_etag(request: Request, pk: int) -> str | None:
return doc.checksum
def thumbnail_last_modified(request: Request, pk: int) -> datetime | None:
def thumbnail_last_modified(request: Any, pk: int) -> datetime | None:
"""
Returns the filesystem last modified either from cache or from filesystem.
Cache should be (slightly?) faster than filesystem
-5
View File
@@ -621,11 +621,6 @@ class ConsumerPlugin(
else:
original_document.save()
# Adding a version changes the effective document, so update root modified
Document.objects.filter(pk=root_doc.pk).update(
modified=timezone.now(),
)
# Create a log entry for the version addition, if enabled
if settings.AUDIT_LOG_ENABLED:
from auditlog.models import ( # type: ignore[import-untyped]
@@ -1,73 +0,0 @@
# Generated by Django 5.2.16 on 2026-07-19 20:33
import django.core.validators
from django.conf import settings
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "0021_widen_workflow_integer_fields"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="document",
name="checksum",
field=models.CharField(
db_index=True,
editable=False,
help_text="The checksum of the original document.",
max_length=64,
verbose_name="checksum",
),
),
migrations.AlterField(
model_name="document",
name="page_count",
field=models.PositiveIntegerField(
db_index=True,
help_text="The number of pages of the document.",
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="page count",
),
),
migrations.AddIndex(
model_name="customfieldinstance",
index=models.Index(
fields=["field", "value_date"],
name="documents_c_field_i_30b51d_idx",
),
),
migrations.AddIndex(
model_name="customfieldinstance",
index=models.Index(
fields=["field", "value_int"],
name="documents_c_field_i_d25ab0_idx",
),
),
migrations.AddIndex(
model_name="customfieldinstance",
index=models.Index(
fields=["field", "value_float"],
name="documents_c_field_i_c35174_idx",
),
),
migrations.AddIndex(
model_name="customfieldinstance",
index=models.Index(
fields=["field", "value_monetary_amount"],
name="documents_c_field_i_53ce4e_idx",
),
),
migrations.AddIndex(
model_name="document",
index=models.Index(
fields=["owner", "created"],
name="documents_d_owner_i_ec6ab3_idx",
),
),
]
+1 -11
View File
@@ -217,7 +217,6 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
_("checksum"),
max_length=64,
editable=False,
db_index=True,
help_text=_("The checksum of the original document."),
)
@@ -235,7 +234,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
blank=False,
null=True,
unique=False,
db_index=True,
db_index=False,
validators=[MinValueValidator(1)],
help_text=_(
"The number of pages of the document.",
@@ -339,9 +338,6 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
ordering = ("-created",)
verbose_name = _("document")
verbose_name_plural = _("documents")
indexes = [
models.Index(fields=["owner", "created"]),
]
constraints = [
models.UniqueConstraint(
fields=["root_document", "version_index"],
@@ -1194,12 +1190,6 @@ class CustomFieldInstance(SoftDeleteModel):
ordering = ("created",)
verbose_name = _("custom field instance")
verbose_name_plural = _("custom field instances")
indexes = [
models.Index(fields=["field", "value_date"]),
models.Index(fields=["field", "value_int"]),
models.Index(fields=["field", "value_float"]),
models.Index(fields=["field", "value_monetary_amount"]),
]
constraints = [
models.UniqueConstraint(
fields=["document", "field"],
@@ -1,6 +1,5 @@
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING
from unittest import TestCase
from unittest import mock
@@ -11,7 +10,6 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
@@ -139,8 +137,6 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
root_document=root,
content="v2-content",
)
original_modified = timezone.now() - datetime.timedelta(days=1)
Document.objects.filter(pk=root.pk).update(modified=original_modified)
with mock.patch("documents.search.get_backend"):
resp = self.client.delete(f"/api/documents/{root.id}/versions/{v2.id}/")
@@ -150,7 +146,6 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.data["current_version_id"], v1.id)
root.refresh_from_db()
self.assertEqual(root.content, "root-content")
self.assertGreater(root.modified, original_modified)
with mock.patch("documents.search.get_backend"):
resp = self.client.delete(f"/api/documents/{root.id}/versions/{v1.id}/")
@@ -331,8 +326,6 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
root_document=root,
version_label="old",
)
original_modified = timezone.now() - datetime.timedelta(days=1)
Document.objects.filter(pk=root.pk).update(modified=original_modified)
resp = self.client.patch(
f"/api/documents/{root.id}/versions/{version.id}/",
@@ -346,8 +339,6 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.data["version_label"], "Label 1")
self.assertEqual(resp.data["id"], version.id)
self.assertFalse(resp.data["is_root"])
root.refresh_from_db()
self.assertGreater(root.modified, original_modified)
def test_update_version_label_clears_on_blank(self) -> None:
root = Document.objects.create(
+9 -18
View File
@@ -1241,7 +1241,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
],
)
def test_selection_data_endpoint(self) -> None:
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")
@@ -1259,28 +1259,30 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
non_matching_doc.tags.add(Tag.objects.create(name="other"))
response = self.client.get(
f"/api/documents/filter_selection_data/?tags__id__in={tag.id}",
f"/api/documents/?tags__id__in={tag.id}&include_selection_data=true",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn("results", response.data)
self.assertIn("selection_data", response.data)
selected_correspondent = next(
item
for item in response.data["selected_correspondents"]
for item in response.data["selection_data"]["selected_correspondents"]
if item["id"] == correspondent.id
)
selected_tag = next(
item for item in response.data["selected_tags"] if item["id"] == tag.id
item
for item in response.data["selection_data"]["selected_tags"]
if item["id"] == tag.id
)
selected_type = next(
item
for item in response.data["selected_document_types"]
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["selected_storage_paths"]
for item in response.data["selection_data"]["selected_storage_paths"]
if item["id"] == storage_path.id
)
@@ -1289,17 +1291,6 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(selected_type["document_count"], 1)
self.assertEqual(selected_storage_path["document_count"], 1)
def test_list_no_longer_supports_include_selection_data(self) -> None:
"""
include_selection_data was never part of a stable release (beta-only,
introduced and removed within the 3.0.0-beta cycle) -- the plain list
endpoint should just ignore the param now rather than compute it inline.
"""
response = self.client.get("/api/documents/?include_selection_data=true")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn("selection_data", response.data)
def test_statistics(self) -> None:
doc1 = Document.objects.create(
title="none1",
-4
View File
@@ -767,8 +767,6 @@ class TestConsumer(
root_doc.archive_serial_number = 42
root_doc.save()
original_modified = timezone.now() - datetime.timedelta(days=1)
Document.objects.filter(pk=root_doc.pk).update(modified=original_modified)
actor = User.objects.create_user(
username="actor",
email="actor@example.com",
@@ -820,8 +818,6 @@ class TestConsumer(
self.assertIsNone(version.archive_serial_number)
self.assertEqual(version.original_filename, version_file.name)
self.assertTrue(bool(version.content))
root_doc.refresh_from_db()
self.assertGreater(root_doc.modified, original_modified)
@override_settings(AUDIT_LOG_ENABLED=True)
@mock.patch("documents.consumer.load_classifier")
@@ -1,12 +1,8 @@
from datetime import timedelta
from types import SimpleNamespace
from typing import TYPE_CHECKING
from typing import cast
from unittest import mock
from django.db import connection
from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from django.utils import timezone
from documents.conditionals import metadata_etag
@@ -17,9 +13,6 @@ from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from documents.versioning import resolve_effective_document_by_pk
if TYPE_CHECKING:
from rest_framework.request import Request
class TestConditionals(DirectoriesMixin, TestCase):
def test_metadata_etag_uses_latest_version_for_root_request(self) -> None:
@@ -36,18 +29,14 @@ class TestConditionals(DirectoriesMixin, TestCase):
mime_type="application/pdf",
root_document=root,
)
request = cast("Request", SimpleNamespace(query_params={}))
request = SimpleNamespace(query_params={})
self.assertEqual(
metadata_etag(request, root.id),
f"{latest.checksum}:{latest.modified.isoformat()}",
)
# preview_etag/thumbnail_etag resolve the same (pk, request) and should
# reuse metadata_etag's cached resolution instead of re-querying.
with CaptureQueriesContext(connection) as ctx:
self.assertEqual(preview_etag(request, root.id), latest.archive_checksum)
self.assertEqual(thumbnail_etag(request, root.id), latest.checksum)
self.assertEqual(len(ctx.captured_queries), 0)
self.assertEqual(preview_etag(request, root.id), latest.archive_checksum)
self.assertEqual(thumbnail_etag(request, root.id), latest.checksum)
def test_metadata_etag_changes_when_document_modified_changes(self) -> None:
doc = Document.objects.create(
@@ -55,20 +44,15 @@ class TestConditionals(DirectoriesMixin, TestCase):
checksum="same-checksum",
mime_type="application/pdf",
)
# Each call simulates a separate incoming HTTP request, so it gets its
# own request object -- the per-request resolution cache must not leak
# a stale result across genuinely different requests.
original_etag = metadata_etag(
cast("Request", SimpleNamespace(query_params={})),
doc.id,
)
request = SimpleNamespace(query_params={})
original_etag = metadata_etag(request, doc.id)
new_modified = timezone.now() + timedelta(seconds=5)
Document.objects.filter(id=doc.id).update(modified=new_modified)
second_request = cast("Request", SimpleNamespace(query_params={}))
self.assertNotEqual(metadata_etag(second_request, doc.id), original_etag)
self.assertNotEqual(metadata_etag(request, doc.id), original_etag)
self.assertEqual(
metadata_etag(second_request, doc.id),
metadata_etag(request, doc.id),
f"{doc.checksum}:{new_modified.isoformat()}",
)
@@ -92,13 +76,9 @@ class TestConditionals(DirectoriesMixin, TestCase):
root_document=other_root,
)
invalid_request = cast(
"Request",
SimpleNamespace(query_params={"version": "not-a-number"}),
)
unrelated_request = cast(
"Request",
SimpleNamespace(query_params={"version": str(other_version.id)}),
invalid_request = SimpleNamespace(query_params={"version": "not-a-number"})
unrelated_request = SimpleNamespace(
query_params={"version": str(other_version.id)},
)
self.assertIsNone(
@@ -125,7 +105,7 @@ class TestConditionals(DirectoriesMixin, TestCase):
latest.thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
latest.thumbnail_path.write_bytes(b"thumb")
request = cast("Request", SimpleNamespace(query_params={}))
request = SimpleNamespace(query_params={})
with mock.patch(
"documents.conditionals.get_thumbnail_modified_key",
return_value="thumb-modified-key",
-63
View File
@@ -2938,69 +2938,6 @@ class TestWorkflows(
self.assertFalse(doc.tags.filter(pk=self.t1.pk).exists())
self.assertTrue(doc.tags.filter(pk=self.t2.pk).exists())
def test_document_updated_workflow_assignment_storage_path_persists_with_tag_assignment(
self,
) -> None:
"""
GIVEN:
- A document updated workflow filtered on a tag
- One assignment action assigns a storage path, a second (later-ordered)
assignment action adds a tag
WHEN:
- The document is updated and the workflow is triggered
THEN:
- Both the tag and the storage path are persisted
"""
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
)
trigger.filter_has_tags.add(self.t1)
assign_storage_path = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.ASSIGNMENT,
assign_storage_path=self.sp,
order=0,
)
assign_tag = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.ASSIGNMENT,
order=1,
)
assign_tag.assign_tags.add(self.t2)
assign_tag.save()
workflow = Workflow.objects.create(
name="Workflow assign storage path then tag",
order=0,
)
workflow.triggers.add(trigger)
workflow.actions.add(assign_storage_path, assign_tag)
workflow.save()
doc = Document.objects.create(
title="sample test",
mime_type="application/pdf",
checksum="assign-tag-and-storage-path",
original_filename="sample.pdf",
)
generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db()
doc.tags.set([self.t1])
superuser = User.objects.create_superuser("superuser")
self.client.force_authenticate(user=superuser)
self.client.patch(
f"/api/documents/{doc.id}/",
{"title": "user update to trigger workflow"},
format="json",
)
doc.refresh_from_db()
self.assertEqual(doc.storage_path, self.sp)
self.assertTrue(doc.tags.filter(pk=self.t2.pk).exists())
def test_removal_action_document_updated_removeall(self) -> None:
"""
GIVEN:
+11 -34
View File
@@ -8,7 +8,7 @@ from typing import Any
from documents.models import Document
if TYPE_CHECKING:
from rest_framework.request import Request
from django.http import HttpRequest
class VersionResolutionError(StrEnum):
@@ -26,7 +26,7 @@ def _document_manager(*, include_deleted: bool) -> Any:
return Document.global_objects if include_deleted else Document.objects
def get_request_version_param(request: Request) -> str | None:
def get_request_version_param(request: HttpRequest) -> str | None:
if hasattr(request, "query_params"):
return request.query_params.get("version")
return None
@@ -57,7 +57,7 @@ def get_latest_version_for_root(
def resolve_requested_version_for_root(
root_doc: Document,
request: Request,
request: Any,
*,
include_deleted: bool = False,
) -> VersionResolution:
@@ -86,7 +86,7 @@ def resolve_requested_version_for_root(
def resolve_effective_document(
request_doc: Document,
request: Request,
request: Any,
*,
include_deleted: bool = False,
) -> VersionResolution:
@@ -107,41 +107,18 @@ def resolve_effective_document(
return VersionResolution(document=request_doc)
_EFFECTIVE_DOCUMENT_CACHE_ATTR = "_effective_document_resolution_cache"
def resolve_effective_document_by_pk(
pk: int,
request: Request,
request: Any,
*,
include_deleted: bool = False,
) -> VersionResolution:
# Django's `condition()` decorator (used for ETag/Last-Modified) invokes the
# etag_func and last_modified_func separately, and the view itself may resolve
# again -- all against the same request. Cache per-request so a single thumb/
# metadata/preview request doesn't redo this resolution multiple times.
cache = getattr(request, _EFFECTIVE_DOCUMENT_CACHE_ATTR, None)
if cache is None:
cache = {}
setattr(request, _EFFECTIVE_DOCUMENT_CACHE_ATTR, cache)
key = (pk, include_deleted)
if key in cache:
return cache[key]
manager = _document_manager(include_deleted=include_deleted)
request_doc = manager.only("id", "root_document_id").filter(pk=pk).first()
if request_doc is None:
resolution = VersionResolution(
document=None,
error=VersionResolutionError.NOT_FOUND,
)
else:
resolution = resolve_effective_document(
request_doc,
request,
include_deleted=include_deleted,
)
cache[key] = resolution
return resolution
return VersionResolution(document=None, error=VersionResolutionError.NOT_FOUND)
return resolve_effective_document(
request_doc,
request,
include_deleted=include_deleted,
)
+19 -29
View File
@@ -1034,11 +1034,7 @@ class DocumentViewSet(
],
}
def _base_document_queryset(self):
# Root documents only, with the annotations that filter_backends rely
# on (effective_content for SearchFilter, num_notes for ordering) --
# but no select_related/prefetch_related, since those only matter for
# serializing documents, not for filtering, ordering, or aggregating.
def get_queryset(self):
latest_version_content = Subquery(
Document.objects.filter(root_document=OuterRef("pk"))
.order_by("-id")
@@ -1050,11 +1046,6 @@ class DocumentViewSet(
.order_by("-created", "-id")
.annotate(effective_content=Coalesce(latest_version_content, F("content")))
.annotate(num_notes=Count("notes"))
)
def get_queryset(self):
return (
self._base_document_queryset()
.select_related("correspondent", "storage_path", "document_type", "owner")
.prefetch_related(
Prefetch(
@@ -1193,21 +1184,24 @@ class DocumentViewSet(
return response
@extend_schema(
operation_id="documents_filter_selection_data",
description=(
"Returns per-tag/correspondent/document-type/storage-path/custom-field "
"document counts for the current filter, without paginating or "
"serializing the matching documents themselves. Split out from the "
"plain document list so that browsing the (potentially huge) unfiltered "
"document list doesn't pay for this aggregation on every request."
),
responses={200: inline_serializer(name="SelectionData", fields={})},
)
@action(detail=False, methods=["get"], url_path="filter_selection_data")
def filter_selection_data(self, request, *args, **kwargs):
queryset = self.filter_queryset(self._base_document_queryset())
return Response(self._get_selection_data_for_queryset(queryset))
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):
from documents.search import get_backend
@@ -2063,8 +2057,6 @@ class DocumentViewSet(
_backend.remove(version_doc.pk)
version_doc_id = version_doc.id
version_doc.delete()
root_doc.modified = timezone.now()
Document.objects.filter(pk=root_doc.pk).update(modified=root_doc.modified)
_backend.add_or_update(root_doc)
if settings.AUDIT_LOG_ENABLED:
actor = (
@@ -2144,8 +2136,6 @@ class DocumentViewSet(
old_label = version_doc.version_label
version_doc.version_label = serializer.validated_data["version_label"]
version_doc.save(update_fields=["version_label"])
root_doc.modified = timezone.now()
Document.objects.filter(pk=root_doc.pk).update(modified=root_doc.modified)
if settings.AUDIT_LOG_ENABLED and old_label != version_doc.version_label:
actor = (
+1 -5
View File
@@ -24,11 +24,7 @@ def apply_assignment_to_document(
action: WorkflowAction, annotated with 'has_assign_*' boolean fields
"""
if action.has_assign_tags:
# Apply to a freshly-fetched instance rather than the shared `document`.
# Document.tags.add() fires an m2m_changed signal that ultimately calls
# instance.refresh_from_db(), which would discard any other unsaved
# assignment fields (e.g. storage_path) already staged on `document`.
Document.objects.get(pk=document.pk).add_nested_tags(action.assign_tags.all())
document.add_nested_tags(action.assign_tags.all())
if action.assign_correspondent:
document.correspondent = action.assign_correspondent
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Amharic\n"
"Language: am_ET\n"
@@ -53,7 +53,7 @@ msgstr "ጥያቄን አይደግፍም expr {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።"
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "ይህ ልማድ አልተገኘም"
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} لا يدعم تعبير الاستعلام {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "لم يتم العثور على حقل مخصص"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Belarusian\n"
"Language: be_BY\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} не поддържа заявка expr {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Надвишена е максималната дълбочина на вмъкване."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Персонализирано поле не е намерено"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Language: ca_ES\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} no suporta expressió de consulta {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Màxima profunditat anidada excedida."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Camp personalitzat no trobat"
@@ -1660,11 +1660,11 @@ msgstr "Permisos insuficients per compartir document %(id)s."
msgid "Bundle is already being processed."
msgstr "Paquet ja s'està processant."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "El paquet de link encarà s'està preparant. Prova de nou més tard."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "El paquet d'enllaç no està disponible."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} nepodporuje výraz dotazu {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Překročena maximální hloubka větvení."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Vlastní pole nebylo nalezeno"
@@ -1661,11 +1661,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Language: da_DK\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} understøtter ikke forespørgsel expr {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Maksimal indlejringsdybde overskredet."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Tilpasset felt ikke fundet"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: German, Switzerland\n"
"Language: de_CH\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
msgid "Maximum nesting depth exceeded."
msgstr "Maximale Verschachtelungstiefe überschritten."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Benutzerdefiniertes Feld nicht gefunden"
@@ -1660,11 +1660,11 @@ msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-17 12:35\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
msgid "Maximum nesting depth exceeded."
msgstr "Maximale Verschachtelungstiefe überschritten."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Zusatzfeld nicht gefunden"
@@ -1660,11 +1660,11 @@ msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@@ -53,7 +53,7 @@ msgstr "Το {data_type} δεν υποστηρίζει το ερώτημα expr
msgid "Maximum nesting depth exceeded."
msgstr "Υπέρβαση μέγιστου βάθους εμφώλευσης."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Το προσαρμοσμένο πεδίο δε βρέθηκε"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} no admite la consulta expr {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Profundidad máxima de nidificación superada."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Campo personalizado no encontrado"
@@ -1660,11 +1660,11 @@ msgstr "Permisos insuficientes para compartir el documento %(id)s."
msgid "Bundle is already being processed."
msgstr "El paquete ya está siendo procesado."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "El paquete de enlace compartido aún está siendo preparado. Por favor, inténtalo de nuevo más tarde."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "El paquete de enlace compartido no está disponible."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Estonian\n"
"Language: et_EE\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} ei toeta päringu avaldist {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Suurim pesastamis sügavus ületatud."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Kohandatud välja ei leitud"
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Language: fa_IR\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr "حداکثر عمق تودرتویی بیش از حد مجاز است."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "زمینه سفارشی یافت نشد"
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Language: fi_FI\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+11 -11
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-21 00:49\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} ne supporte pas l'expression {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Profondeur de récursion maximale dépassée."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Champ personnalisé non trouvé"
@@ -1644,7 +1644,7 @@ msgstr "Configuration IA invalide."
#: documents/views.py:1522
msgid "AI backend request timed out."
msgstr ""
msgstr "La requête d'arrière-plan IA a expiré."
#: documents/views.py:2304 documents/views.py:2625
msgid "Specify only one of text, title_search, query, or more_like_id."
@@ -1659,11 +1659,11 @@ msgstr "Droits d'accès insuffisant pour partager %(id)s document."
msgid "Bundle is already being processed."
msgstr "Le paquet est déjà en cours de traitement."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
@@ -1705,7 +1705,7 @@ msgstr "rétablir"
#: paperless/models.py:42
msgid "off"
msgstr ""
msgstr "off"
#: paperless/models.py:51
msgid "always"
@@ -1909,11 +1909,11 @@ msgstr "Définit le endpoint LLM, optionnel"
#: paperless/models.py:363
msgid "Sets the LLM output language"
msgstr ""
msgstr "Définit la langue de sortie du LLM"
#: paperless/models.py:370
msgid "Sets the LLM timeout in seconds"
msgstr ""
msgstr "Définit le délai d'attente du LLM en secondes"
#: paperless/models.py:376
msgid "paperless application settings"
@@ -2373,11 +2373,11 @@ msgstr "Affecter le propriétaire de la règle aux documents"
#: paperless_mail/models.py:305
msgid "Stop processing further rules"
msgstr ""
msgstr "Arrêter le traitement des règles suivantes"
#: paperless_mail/models.py:308
msgid "If True, no further rules will be processed after this one if any document is queued."
msgstr ""
msgstr "Si Vrai, aucune règle supplémentaire ne sera traitée après celle-ci si un document est mis en attente."
#: paperless_mail/models.py:334
msgid "uid"
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Language: he_IL\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} לא תומך בביטוי שאילתה {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "חריגה מעומק הקינון המרבי."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "שדה מותאם אישית לא נמצא"
@@ -1661,11 +1661,11 @@ msgstr "הרשאות לא מספיקות לשיתוף מסמך %(id)s."
msgid "Bundle is already being processed."
msgstr "החבילה (Bundle) כבר נמצאת בתהליך עיבוד."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "חבילת קישור השיתוף עדיין בהכנה. נא לנסות שוב מאוחר יותר."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "חבילת קישור השיתוף אינה זמינה."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Language: hi_IN\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} क्वेरी एक्सप्रेशन {expr!r}
msgid "Maximum nesting depth exceeded."
msgstr "अधिकतम नेस्टिंग डेप्थ पार हो गई है।"
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "कस्टम फ़ील्ड नहीं मिला"
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} ne podržava upit izraz {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Premašena je najveća razina ugniježđivanja."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Prilagođeno polje nije pronađeno"
@@ -1660,11 +1660,11 @@ msgstr "Nedovoljne ovlasti za dijeljenje dokumenta %(id)s."
msgid "Bundle is already being processed."
msgstr "Paket se već obrađuje."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Paket linka za dijeljenje se još priprema. Pokušajte ponovo kasnije."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "Paket linka za dijeljenje nije dostupan."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Language: hu_HU\n"
@@ -53,7 +53,7 @@ msgstr "A(z) {data_type} nem támogatja a {expr!r} kifejezés lekérdezést."
msgid "Maximum nesting depth exceeded."
msgstr "Maximum beágyazási mélység túllépve."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Az egyéni mező nem található"
@@ -1660,11 +1660,11 @@ msgstr "Nincs megfelelő jogosultság a %(id)s dokumentum megosztásához."
msgid "Bundle is already being processed."
msgstr "A csomag feldolgozása már folyamatban van."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "A megosztási linkcsomag készítése folyamatban. Kérjük, próbálja meg később."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "A megosztási linkcsomag nem elérhető."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-20 12:47\n"
"Last-Translator: \n"
"Language-Team: Indonesian\n"
"Language: id_ID\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} tidak mendukung ekspresi pencarian expr {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Kedalaman susunan maksimal terlampaui."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Bidang khusus tidak ditemukan"
@@ -1660,11 +1660,11 @@ msgstr "Izin tidak mencukupi untuk berbagi dokumen %(id)s"
msgid "Bundle is already being processed."
msgstr "Paket sedang diproses."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Bundel tautan berbagi masih dalam proses persiapan. Silakan coba lagi nanti."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "Bundel tautan berbagi tidak tersedia."
+6 -6
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} Non supporta la jQuery Expo {Expo!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Profondità massima di nidificazione superata."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Campo personalizzato non trovato"
@@ -1660,11 +1660,11 @@ msgstr "Autorizzazioni insufficienti per condividere il documento %(id)s."
msgid "Bundle is already being processed."
msgstr "Il pacchetto è già in fase di elaborazione."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Il pacchetto di link di condivisione è ancora in fase di preparazione. Riprova più tardi."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "Il pacchetto di link di condivisione non è disponibile."
@@ -2386,7 +2386,7 @@ msgstr "uid"
#: paperless_mail/models.py:342
msgid "uid validity"
msgstr ""
msgstr "validità uid"
#: paperless_mail/models.py:350
msgid "subject"
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} はクエリ expr {expr!r} をサポートしていません
msgid "Maximum nesting depth exceeded."
msgstr "最大ネストの深さを超えました。"
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "カスタムフィールドが見つかりません"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -53,7 +53,7 @@ msgstr "{data_type}은 쿼리 표현식 {expr!r}을(를) 지원하지 않습니
msgid "Maximum nesting depth exceeded."
msgstr "최대 중첩 깊이를 초과했습니다."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "사용자 지정 필드를 찾을 수 없음"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Luxembourgish\n"
"Language: lb_LU\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Lithuanian\n"
"Language: lt_LT\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} nepalaiko užklausos išraiškos {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Viršytas maksimalus įdėjimo gylis."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Pasirinktinis laukas nerastas"
@@ -1659,11 +1659,11 @@ msgstr "Nepakanka leidimų bendrinti dokumentą %(id)s."
msgid "Bundle is already being processed."
msgstr "Rinkinys jau apdorojamas."
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Bendrinimo nuorodų rinkinys vis dar ruošiamas. Bandykite dar kartą vėliau."
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr "Dalinimos nuorodų rinkinys yra nepasiekiamas."
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Latvian\n"
"Language: lv_LV\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Macedonian\n"
"Language: mk_MK\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Malay\n"
"Language: ms_MY\n"
@@ -53,7 +53,7 @@ msgstr ""
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr ""
@@ -1659,11 +1659,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""
+5 -5
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"PO-Revision-Date: 2026-07-15 20:45\n"
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
"PO-Revision-Date: 2026-07-19 20:57\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -53,7 +53,7 @@ msgstr "{data_type} ondersteunt geen query expr {expr!r}."
msgid "Maximum nesting depth exceeded."
msgstr "Maximale nestdiepte overschreden."
#: documents/filters.py:1052
#: documents/filters.py:1059
msgid "Custom field not found"
msgstr "Aangepast veld niet gevonden"
@@ -1660,11 +1660,11 @@ msgstr ""
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4554
#: documents/views.py:4555
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4564
#: documents/views.py:4565
msgid "The share link bundle is unavailable."
msgstr ""

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