Chore: Paginate the task listing (#12633)

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
Trenton H 2026-04-24 10:31:37 -07:00 committed by GitHub
parent dbce393604
commit d6e45093e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 260 additions and 64 deletions

View File

@ -98,6 +98,18 @@
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
}
<ngb-pagination
class="ms-md-3 mb-0"
[pageSize]="pageSize"
[collectionSize]="totalTasks"
[page]="page"
[maxSize]="5"
[rotate]="true"
size="sm"
aria-label="Tasks pagination"
(pageChange)="setPage($event)">
</ngb-pagination>
</div>
<ng-template let-tasks="tasks" let-section="section" #tasksTemplate>

View File

@ -19,6 +19,7 @@ import {
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { Results } from 'src/app/data/results'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@ -148,6 +149,11 @@ const tasks: PaperlessTask[] = [
},
]
const paginatedTasks: Results<PaperlessTask> = {
count: tasks.length,
results: tasks,
}
describe('TasksComponent', () => {
let component: TasksComponent
let fixture: ComponentFixture<TasksComponent>
@ -196,9 +202,25 @@ describe('TasksComponent', () => {
component = fixture.componentInstance
jest.useFakeTimers()
fixture.detectChanges()
httpTestingController
.expectOne(`${environment.apiBaseUrl}tasks/?acknowledged=false`)
.flush(tasks)
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
.flush(paginatedTasks)
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '25' &&
req.params.get('page') === '1'
)
.flush(paginatedTasks)
})
it('should display task sections with counts', () => {
@ -294,6 +316,40 @@ describe('TasksComponent', () => {
).toBeTruthy()
})
it('should render pagination controls next to the task filter', () => {
fixture.detectChanges()
const controls = fixture.debugElement.query(By.css('.task-controls'))
const search = controls.query(By.css('.task-search'))
const pagination = controls.query(By.css('ngb-pagination'))
expect(search).not.toBeNull()
expect(pagination).not.toBeNull()
})
it('should load a different task page when pagination changes', () => {
component.setPage(2)
const pageTwoTasks = {
count: 30,
results: [tasks[0]],
}
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '25' &&
req.params.get('page') === '2'
)
.flush(pageTwoTasks)
expect(component.page).toBe(2)
expect(component.totalTasks).toBe(30)
expect(component.pagedTasks).toEqual([tasks[0]])
})
it('should expose stable task type options and disable empty ones', () => {
expect(component.taskTypeOptions.map((option) => option.value)).toContain(
PaperlessTaskType.TrainClassifier
@ -470,7 +526,7 @@ describe('TasksComponent', () => {
component.toggleSection(TaskSection.NeedsAttention, {
target: { checked: false },
} as PointerEvent)
} as unknown as PointerEvent)
expect(component.selectedTasks).toEqual(new Set())
})

View File

@ -6,6 +6,7 @@ import {
NgbCollapseModule,
NgbDropdownModule,
NgbModal,
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@ -139,6 +140,7 @@ const TRIGGER_SOURCE_OPTIONS: Array<{
NgTemplateOutlet,
NgbCollapseModule,
NgbDropdownModule,
NgbPaginationModule,
NgbPopoverModule,
NgxBootstrapIconsModule,
],
@ -161,6 +163,10 @@ export class TasksComponent
public selectedTasks: Set<number> = new Set()
public expandedTask: number
public autoRefreshEnabled: boolean = true
public readonly pageSize = 25
public page: number = 1
public totalTasks: number = 0
public pagedTasks: PaperlessTask[] = []
public selectedSection: TaskSection = TaskSection.All
public selectedTaskType: PaperlessTaskType | null = null
public selectedTriggerSource: PaperlessTaskTriggerSource | null = null
@ -254,6 +260,7 @@ export class TasksComponent
ngOnInit() {
this.tasksService.reload()
this.reloadPage()
timer(5000, 5000)
.pipe(
filter(() => this.autoRefreshEnabled),
@ -261,6 +268,7 @@ export class TasksComponent
)
.subscribe(() => {
this.tasksService.reload()
this.reloadPage(false)
})
this.filterDebounce
@ -270,7 +278,10 @@ export class TasksComponent
distinctUntilChanged(),
filter((query) => !query.length || query.length > 2)
)
.subscribe((query) => (this._filterText = query))
.subscribe((query) => {
this._filterText = query
this.clearSelection()
})
}
ngOnDestroy() {
@ -300,6 +311,9 @@ export class TasksComponent
modal.componentInstance.buttonsEnabled = false
modal.close()
this.tasksService.dismissTasks(tasks).subscribe({
next: () => {
this.reloadPage(false)
},
error: (e) => {
this.toastService.showError($localize`Error dismissing tasks`, e)
modal.componentInstance.buttonsEnabled = true
@ -309,6 +323,9 @@ export class TasksComponent
})
} else if (tasks.size === 1) {
this.tasksService.dismissTasks(tasks).subscribe({
next: () => {
this.reloadPage(false)
},
error: (e) =>
this.toastService.showError($localize`Error dismissing task`, e),
})
@ -421,7 +438,7 @@ export class TasksComponent
}
tasksForSection(section: TaskSection): PaperlessTask[] {
let tasks = this.tasksService.allFileTasks.filter((task) =>
let tasks = this.pagedTasks.filter((task) =>
this.taskBelongsToSection(task, section)
)
@ -433,7 +450,7 @@ export class TasksComponent
}
sectionCount(section: TaskSection): number {
return this.tasksService.allFileTasks.filter((task) =>
return this.pagedTasks.filter((task) =>
this.taskBelongsToSection(task, section)
).length
}
@ -481,6 +498,16 @@ export class TasksComponent
this.selectedTasks.clear()
}
setPage(page: number) {
if (this.page === page) {
return
}
this.page = page
this.clearSelection()
this.reloadPage()
}
public resetFilter() {
this._filterText = ''
}
@ -576,10 +603,39 @@ export class TasksComponent
? this.sections
: [this.selectedSection]
return this.tasksService.allFileTasks.filter(
return this.pagedTasks.filter(
(task) =>
sections.some((section) => this.taskBelongsToSection(task, section)) &&
this.taskMatchesFilters(task, { taskType, triggerSource })
)
}
private reloadPage(resetToFirstPage: boolean = false) {
if (resetToFirstPage) {
this.page = 1
}
this.loading = true
this.tasksService
.list(this.page, this.pageSize, { acknowledged: false })
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.pagedTasks = result.results
this.totalTasks = result.count
this.loading = false
if (
this.page > 1 &&
this.pagedTasks.length === 0 &&
this.totalTasks > 0
) {
this.page -= 1
this.reloadPage()
}
},
error: () => {
this.loading = false
},
})
}
}

View File

@ -1,4 +1,8 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
HttpRequest,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http'
import {
HttpTestingController,
provideHttpClientTesting,
@ -37,16 +41,21 @@ describe('TasksService', () => {
it('calls tasks api endpoint on reload', () => {
tasksService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}tasks/?acknowledged=false`
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
expect(req.request.method).toEqual('GET')
req.flush({ count: 0, results: [] })
})
it('does not call tasks api endpoint on reload if already loading', () => {
tasksService.loading = true
tasksService.reload()
httpTestingController.expectNone(
`${environment.apiBaseUrl}tasks/?acknowledged=false`
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
})
@ -62,8 +71,13 @@ describe('TasksService', () => {
req.flush([])
// reload is then called
httpTestingController
.expectOne(`${environment.apiBaseUrl}tasks/?acknowledged=false`)
.flush([])
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
.flush({ count: 0, results: [] })
})
it('groups mixed task types by status when reloading', () => {
@ -124,10 +138,13 @@ describe('TasksService', () => {
tasksService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}tasks/?acknowledged=false`
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
req.flush(mockTasks)
req.flush({ count: mockTasks.length, results: mockTasks })
expect(tasksService.allFileTasks).toHaveLength(5)
expect(tasksService.completedFileTasks).toHaveLength(2)
@ -173,10 +190,13 @@ describe('TasksService', () => {
tasksService.reload()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}tasks/?acknowledged=false`
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
req.flush(mockTasks)
req.flush({ count: mockTasks.length, results: mockTasks })
expect(tasksService.needsAttentionTasks).toHaveLength(2)
expect(tasksService.needsAttentionTasks.map((task) => task.status)).toEqual(

View File

@ -1,12 +1,13 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { first, takeUntil, tap } from 'rxjs/operators'
import { first, map, takeUntil, tap } from 'rxjs/operators'
import {
PaperlessTask,
PaperlessTaskStatus,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { Results } from 'src/app/data/results'
import { environment } from 'src/environments/environment'
@Injectable({
@ -17,6 +18,7 @@ export class TasksService {
private baseUrl: string = environment.apiBaseUrl
private endpoint: string = 'tasks'
private readonly defaultReloadPageSize = 1000
public loading: boolean = false
@ -69,9 +71,13 @@ export class TasksService {
this.loading = true
this.http
.get<PaperlessTask[]>(
`${this.baseUrl}${this.endpoint}/?acknowledged=false`
)
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
acknowledged: 'false',
page_size: this.defaultReloadPageSize,
},
})
.pipe(map((r) => r.results))
.pipe(takeUntil(this.unsubscribeNotifer), first())
.subscribe((r) => {
this.fileTasks = r
@ -79,6 +85,23 @@ export class TasksService {
})
}
public list(
page: number,
pageSize: number,
extraParams?: Record<string, string | number | boolean>
): Observable<Results<PaperlessTask>> {
return this.http.get<Results<PaperlessTask>>(
`${this.baseUrl}${this.endpoint}/`,
{
params: {
page,
page_size: pageSize,
...extraParams,
},
}
)
}
public dismissTasks(task_ids: Set<number>): Observable<any> {
return this.http
.post(`${this.baseUrl}tasks/acknowledge/`, {

View File

@ -31,6 +31,20 @@ ACCEPT_V9 = "application/json; version=9"
@pytest.mark.django_db()
class TestGetTasksV10:
def test_list_response_has_paginated_structure(
self,
admin_client: APIClient,
) -> None:
"""GET /api/tasks/ returns a paginated envelope with count and results."""
PaperlessTaskFactory.create_batch(3)
response = admin_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert "count" in response.data
assert "results" in response.data
assert response.data["count"] == 3
def test_list_returns_tasks(self, admin_client: APIClient) -> None:
"""GET /api/tasks/ returns all tasks visible to the admin."""
PaperlessTaskFactory.create_batch(2)
@ -38,7 +52,7 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 2
assert response.data["count"] == 2
def test_related_document_ids_populated_from_result_data(
self,
@ -53,7 +67,7 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["related_document_ids"] == [7]
assert response.data["results"][0]["related_document_ids"] == [7]
def test_related_document_ids_includes_duplicate_of(
self,
@ -68,7 +82,7 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["related_document_ids"] == [12]
assert response.data["results"][0]["related_document_ids"] == [12]
def test_filter_by_task_type(self, admin_client: APIClient) -> None:
"""?task_type= filters results to tasks of that type only."""
@ -81,8 +95,11 @@ class TestGetTasksV10:
)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["task_type"] == PaperlessTask.TaskType.TRAIN_CLASSIFIER
assert response.data["count"] == 1
assert (
response.data["results"][0]["task_type"]
== PaperlessTask.TaskType.TRAIN_CLASSIFIER
)
def test_filter_by_status(self, admin_client: APIClient) -> None:
"""?status= filters results to tasks with that status only."""
@ -95,8 +112,8 @@ class TestGetTasksV10:
)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["status"] == PaperlessTask.Status.SUCCESS
assert response.data["count"] == 1
assert response.data["results"][0]["status"] == PaperlessTask.Status.SUCCESS
def test_filter_by_task_id(self, admin_client: APIClient) -> None:
"""?task_id= returns only the task with that UUID."""
@ -106,8 +123,8 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT, {"task_id": task.task_id})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["task_id"] == task.task_id
assert response.data["count"] == 1
assert response.data["results"][0]["task_id"] == task.task_id
def test_filter_by_acknowledged(self, admin_client: APIClient) -> None:
"""?acknowledged=false returns only tasks that have not been acknowledged."""
@ -117,8 +134,8 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT, {"acknowledged": "false"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["acknowledged"] is False
assert response.data["count"] == 1
assert response.data["results"][0]["acknowledged"] is False
def test_filter_is_complete_true(self, admin_client: APIClient) -> None:
"""?is_complete=true returns only SUCCESS and FAILURE tasks."""
@ -129,8 +146,8 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT, {"is_complete": "true"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 2
returned_statuses = {t["status"] for t in response.data}
assert response.data["count"] == 2
returned_statuses = {t["status"] for t in response.data["results"]}
assert returned_statuses == {
PaperlessTask.Status.SUCCESS,
PaperlessTask.Status.FAILURE,
@ -145,8 +162,8 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT, {"is_complete": "false"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 2
returned_statuses = {t["status"] for t in response.data}
assert response.data["count"] == 2
returned_statuses = {t["status"] for t in response.data["results"]}
assert returned_statuses == {
PaperlessTask.Status.PENDING,
PaperlessTask.Status.STARTED,
@ -162,7 +179,7 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
ids = [t["task_id"] for t in response.data]
ids = [t["task_id"] for t in response.data["results"]]
assert ids == [t3.task_id, t2.task_id, t1.task_id]
def test_list_scoped_to_own_and_unowned_tasks_for_regular_user(
@ -186,8 +203,8 @@ class TestGetTasksV10:
response = client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 2
visible_ids = {t["task_id"] for t in response.data}
assert response.data["count"] == 2
visible_ids = {t["task_id"] for t in response.data["results"]}
assert visible_ids == {own_task.task_id, unowned_task.task_id}
def test_list_admin_sees_all_tasks(
@ -204,7 +221,7 @@ class TestGetTasksV10:
response = admin_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 3
assert response.data["count"] == 3
@pytest.mark.django_db()
@ -241,7 +258,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["task_name"] == expected_task_name
assert response.data["results"][0]["task_name"] == expected_task_name
@pytest.mark.parametrize(
("trigger_source", "expected_type"),
@ -295,7 +312,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["type"] == expected_type
assert response.data["results"][0]["type"] == expected_type
def test_task_file_name_from_input_data(self, v9_client: APIClient) -> None:
"""task_file_name is read from input_data['filename']."""
@ -304,7 +321,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["task_file_name"] == "report.pdf"
assert response.data["results"][0]["task_file_name"] == "report.pdf"
def test_task_file_name_none_when_no_filename_key(
self,
@ -316,7 +333,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["task_file_name"] is None
assert response.data["results"][0]["task_file_name"] is None
def test_related_document_from_result_data_document_id(
self,
@ -331,7 +348,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["related_document"] == 99
assert response.data["results"][0]["related_document"] == 99
def test_related_document_none_when_no_result_data(
self,
@ -343,7 +360,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["related_document"] is None
assert response.data["results"][0]["related_document"] is None
def test_duplicate_documents_from_result_data(self, v9_client: APIClient) -> None:
"""duplicate_documents includes duplicate_of from result_data in v9."""
@ -356,7 +373,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
dupes = response.data[0]["duplicate_documents"]
dupes = response.data["results"][0]["duplicate_documents"]
assert len(dupes) == 1
assert dupes[0]["id"] == doc.pk
assert dupes[0]["title"] == doc.title
@ -372,7 +389,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["duplicate_documents"] == []
assert response.data["results"][0]["duplicate_documents"] == []
def test_status_remapped_to_uppercase(self, v9_client: APIClient) -> None:
"""v9 status values are uppercase Celery state strings."""
@ -383,7 +400,7 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
statuses = {t["status"] for t in response.data}
statuses = {t["status"] for t in response.data["results"]}
assert statuses == {"SUCCESS", "PENDING", "FAILURE"}
def test_filter_by_task_name_maps_old_value(self, v9_client: APIClient) -> None:
@ -394,8 +411,8 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT, {"task_name": "check_sanity"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["task_name"] == "check_sanity"
assert response.data["count"] == 1
assert response.data["results"][0]["task_name"] == "check_sanity"
def test_v9_non_staff_sees_own_and_unowned_tasks(
self,
@ -418,7 +435,7 @@ class TestGetTasksV9:
response = client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 2
assert response.data["count"] == 2
def test_filter_by_task_name_maps_to_task_type(self, v9_client: APIClient) -> None:
"""?task_name=consume_file filter maps to the task_type field for v9 compatibility."""
@ -428,8 +445,8 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT, {"task_name": "consume_file"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["task_name"] == "consume_file"
assert response.data["count"] == 1
assert response.data["results"][0]["task_name"] == "consume_file"
def test_filter_by_type_scheduled_task(self, v9_client: APIClient) -> None:
"""?type=scheduled_task matches trigger_source=scheduled only."""
@ -439,8 +456,8 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT, {"type": "scheduled_task"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
assert response.data[0]["type"] == "scheduled_task"
assert response.data["count"] == 1
assert response.data["results"][0]["type"] == "scheduled_task"
def test_filter_by_type_auto_task_includes_all_auto_sources(
self,
@ -457,8 +474,8 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT, {"type": "auto_task"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 3
assert all(t["type"] == "auto_task" for t in response.data)
assert response.data["count"] == 3
assert all(t["type"] == "auto_task" for t in response.data["results"])
def test_filter_by_type_manual_task_includes_all_manual_sources(
self,
@ -475,8 +492,8 @@ class TestGetTasksV9:
response = v9_client.get(ENDPOINT, {"type": "manual_task"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 3
assert all(t["type"] == "manual_task" for t in response.data)
assert response.data["count"] == 3
assert all(t["type"] == "manual_task" for t in response.data["results"])
@pytest.mark.django_db()
@ -510,7 +527,7 @@ class TestAcknowledge:
response = admin_client.get(ENDPOINT, {"acknowledged": "false"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 0
assert response.data["count"] == 0
def test_requires_change_permission(self, user_client: APIClient) -> None:
"""Regular users without change_paperlesstask permission receive 403."""
@ -828,7 +845,7 @@ class TestDuplicateDocumentsPermissions:
response = user_v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
dupes = response.data[0]["duplicate_documents"]
dupes = response.data["results"][0]["duplicate_documents"]
assert len(dupes) == 1
assert dupes[0]["id"] == doc.pk
@ -848,7 +865,7 @@ class TestDuplicateDocumentsPermissions:
response = user_v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert len(response.data[0]["duplicate_documents"]) == 1
assert len(response.data["results"][0]["duplicate_documents"]) == 1
def test_other_users_duplicate_document_is_hidden(
self,
@ -867,7 +884,7 @@ class TestDuplicateDocumentsPermissions:
response = user_v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data[0]["duplicate_documents"] == []
assert response.data["results"][0]["duplicate_documents"] == []
def test_explicit_permission_grants_visibility(
self,
@ -887,6 +904,6 @@ class TestDuplicateDocumentsPermissions:
response = user_v9_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
dupes = response.data[0]["duplicate_documents"]
dupes = response.data["results"][0]["duplicate_documents"]
assert len(dupes) == 1
assert dupes[0]["id"] == doc.pk

View File

@ -69,6 +69,7 @@ from django.views.decorators.http import condition
from django.views.decorators.http import last_modified
from django.views.generic import TemplateView
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.utils import extend_schema
@ -3799,6 +3800,15 @@ class RemoteVersionView(GenericAPIView[Any]):
)
class _TasksViewSetSchema(AutoSchema):
_UNPAGINATED_ACTIONS = frozenset({"summary", "active"})
def _get_paginator(self):
if getattr(self.view, "action", None) in self._UNPAGINATED_ACTIONS:
return None
return super()._get_paginator()
@extend_schema_view(
list=extend_schema(
parameters=[
@ -3857,7 +3867,9 @@ class RemoteVersionView(GenericAPIView[Any]):
),
)
class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
schema = _TasksViewSetSchema()
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
pagination_class = StandardPagination
filter_backends = (
DjangoFilterBackend,
OrderingFilter,