diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html index 57197d1ea..cef3690d1 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -34,7 +34,7 @@
+ *ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index b324ac6a0..79548aaf8 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -12,6 +12,7 @@ import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dro import { MatchingModel } from 'src/app/data/matching-model' import { Subject } from 'rxjs' import { SelectionDataItem } from 'src/app/services/rest/document.service' +import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' export interface ChangedItems { itemsToAdd: MatchingModel[] @@ -552,4 +553,13 @@ export class FilterableDropdownComponent { // just track the index in case user uses arrows this.keyboardIndex = index } + + hideCount(item: ObjectWithPermissions) { + // counts are pointless when clicking item would add to the set of docs + return ( + this.selectionModel.logicalOperator === LogicalOperator.Or && + this.manyToOne && + this.selectionModel.get(item.id) !== ToggleableItemState.Selected + ) + } } diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html index af935b0db..b67ae736c 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html @@ -20,5 +20,5 @@ {{item.name}} -
{{count ?? item.document_count}}
+
{{count ?? item.document_count}}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts index 32ec671b1..7eb6d1b26 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts @@ -26,6 +26,9 @@ export class ToggleableDropdownButtonComponent { @Input() disabled: boolean = false + @Input() + hideCount: boolean = false + @Output() toggle = new EventEmitter() diff --git a/src-ui/src/app/data/results.ts b/src-ui/src/app/data/results.ts index dbf99c5a1..d29a55567 100644 --- a/src-ui/src/app/data/results.ts +++ b/src-ui/src/app/data/results.ts @@ -2,4 +2,6 @@ export interface Results { count: number results: T[] + + all: number[] } diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 906e36a0f..6245c6632 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' import { ParamMap, Router } from '@angular/router' -import { Observable } from 'rxjs' +import { Observable, first } from 'rxjs' import { filterRulesDiffer, cloneFilterRules, @@ -230,7 +230,8 @@ export class DocumentListViewService { activeListViewState.documents = result.results this.documentService - .getSelectionData(result.results.map((d) => d.id)) + .getSelectionData(result.all) + .pipe(first()) .subscribe({ next: (selectionData) => { this.selectionData = selectionData diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 71ffe7be2..0a10b47fe 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -424,7 +424,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer): def to_representation(self, instance): doc = super().to_representation(instance) - if self.truncate_content: + if self.truncate_content and "content" in self.fields: doc["content"] = doc.get("content")[0:550] return doc diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index 6c08b6955..fbb0b5eb5 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -499,21 +499,25 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): results = response.data["results"] self.assertEqual(response.data["count"], 3) self.assertEqual(len(results), 3) + self.assertCountEqual(response.data["all"], [d1.id, d2.id, d3.id]) response = self.client.get("/api/documents/?query=september") results = response.data["results"] self.assertEqual(response.data["count"], 1) self.assertEqual(len(results), 1) + self.assertCountEqual(response.data["all"], [d3.id]) response = self.client.get("/api/documents/?query=statement") results = response.data["results"] self.assertEqual(response.data["count"], 2) self.assertEqual(len(results), 2) + self.assertCountEqual(response.data["all"], [d2.id, d3.id]) response = self.client.get("/api/documents/?query=sfegdfg") results = response.data["results"] self.assertEqual(response.data["count"], 0) self.assertEqual(len(results), 0) + self.assertCountEqual(response.data["all"], []) def test_search_multi_page(self): with AsyncWriter(index.open_index()) as writer: @@ -1248,6 +1252,31 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): [d1.id, d3.id, d2.id], ) + def test_pagination_all(self): + """ + GIVEN: + - A set of 50 documents + WHEN: + - API reuqest for document filtering + THEN: + - Results are paginated (25 items) and response["all"] returns all ids (50 items) + """ + t = Tag.objects.create(name="tag") + docs = [] + for i in range(50): + d = Document.objects.create(checksum=i, content=f"test{i}") + d.tags.add(t) + docs.append(d) + + response = self.client.get( + f"/api/documents/?tags__id__in={t.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 25) + self.assertEqual(len(response.data["all"]), 50) + self.assertCountEqual(response.data["all"], [d.id for d in docs]) + def test_statistics(self): doc1 = Document.objects.create( title="none1", diff --git a/src/paperless/views.py b/src/paperless/views.py index 588b534e3..777641f75 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -1,4 +1,5 @@ import os +from collections import OrderedDict from django.contrib.auth.models import Group from django.contrib.auth.models import User @@ -9,6 +10,7 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from documents.permissions import PaperlessObjectPermissions @@ -23,6 +25,47 @@ class StandardPagination(PageNumberPagination): page_size_query_param = "page_size" max_page_size = 100000 + def get_paginated_response(self, data): + return Response( + OrderedDict( + [ + ("count", self.page.paginator.count), + ("next", self.get_next_link()), + ("previous", self.get_previous_link()), + ("all", self.get_all_result_ids()), + ("results", data), + ], + ), + ) + + def get_all_result_ids(self): + ids = [] + if hasattr(self.page.paginator.object_list, "saved_results"): + results_page = self.page.paginator.object_list.saved_results[0] + if results_page is not None: + for i in range(0, len(results_page.results.docs())): + try: + fields = results_page.results.fields(i) + if "id" in fields: + ids.append(fields["id"]) + except Exception: + pass + else: + for obj in self.page.paginator.object_list: + if hasattr(obj, "id"): + ids.append(obj.id) + elif hasattr(obj, "fields"): + ids.append(obj.fields()["id"]) + return ids + + def get_paginated_response_schema(self, schema): + response_schema = super().get_paginated_response_schema(schema) + response_schema["properties"]["all"] = { + "type": "array", + "example": "[1, 2, 3]", + } + return response_schema + class FaviconView(View): def get(self, request, *args, **kwargs): # pragma: nocover