diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 7abb95823..314910f5d 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -5942,7 +5942,7 @@ src/app/components/document-list/document-list.component.html - 289 + 294 @@ -5957,7 +5957,7 @@ src/app/components/document-list/document-list.component.html - 329 + 334 @@ -5972,7 +5972,7 @@ src/app/components/document-list/document-list.component.html - 336 + 341 @@ -7209,7 +7209,7 @@ src/app/components/document-list/document-list.component.html - 305 + 310 @@ -7573,25 +7573,32 @@ 261,263 + + Sort by + + src/app/components/document-list/document-list.component.html + 268,269 + + Edit document src/app/components/document-list/document-list.component.html - 297 + 302 Preview document src/app/components/document-list/document-list.component.html - 298 + 303 Yes src/app/components/document-list/document-list.component.html - 357 + 362 src/app/pipes/yes-no.pipe.ts @@ -7602,7 +7609,7 @@ No src/app/components/document-list/document-list.component.html - 357 + 362 src/app/pipes/yes-no.pipe.ts diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index fdf3e24ed..5ad3aa6c7 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -262,9 +262,14 @@ Shared } - @for (field of activeDisplayCustomFields; track field) { - - {{getDisplayCustomFieldTitle(field)}} + @for (field_id of activeDisplayCustomFields; track field_id) { + + {{getDisplayCustomFieldTitle(field_id)}} } diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts index 9ff50f1bc..9372c94ff 100644 --- a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts +++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts @@ -11,6 +11,7 @@ import { SavedView } from 'src/app/data/saved-view' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsService } from 'src/app/services/permissions.service' +import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { ToastService } from 'src/app/services/toast.service' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' @@ -60,6 +61,17 @@ describe('SavedViewsComponent', () => { currentUserCan: () => true, }, }, + { + provide: CustomFieldsService, + useValue: { + listAll: () => + of({ + all: [], + count: 0, + results: [], + }), + }, + }, PermissionsGuard, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), diff --git a/src-ui/src/app/services/document-list-view.service.spec.ts b/src-ui/src/app/services/document-list-view.service.spec.ts index 2ef084b17..d93acc521 100644 --- a/src-ui/src/app/services/document-list-view.service.spec.ts +++ b/src-ui/src/app/services/document-list-view.service.spec.ts @@ -156,7 +156,7 @@ describe('DocumentListViewService', () => { expect(documentListViewService.currentPage).toEqual(1) }) - it('should handle error on filtering request', () => { + it('should handle object error on filtering request', () => { documentListViewService.currentPage = 1 const tags__id__in = 'hello' const filterRulesAny = [ @@ -185,6 +185,50 @@ describe('DocumentListViewService', () => { ) }) + it('should handle object error on filtering request for custom field sorts', () => { + 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` + ) + expect(req.request.method).toEqual('GET') + req.flush( + { custom_field_999: ['Custom field not found'] }, + { status: 400, statusText: 'Unexpected error' } + ) + expect(documentListViewService.error).toEqual( + 'custom_field_999: Custom field not found' + ) + // reset the list + documentListViewService.sortField = 'created' + req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true` + ) + }) + + it('should handle string error on filtering request', () => { + documentListViewService.currentPage = 1 + const tags__id__in = 'hello' + const filterRulesAny = [ + { + rule_type: FILTER_HAS_TAGS_ANY, + value: tags__id__in, + }, + ] + documentListViewService.filterRules = filterRulesAny + let req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}` + ) + expect(req.request.method).toEqual('GET') + req.flush('Generic error', { status: 404, statusText: 'Unexpected error' }) + expect(documentListViewService.error).toEqual('Generic error') + // reset the list + documentListViewService.filterRules = [] + req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true` + ) + }) + it('should support setting sort', () => { expect(documentListViewService.sortField).toEqual('created') expect(documentListViewService.sortReverse).toBeTruthy() 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 e15c11d3a..893bfca91 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -307,18 +307,23 @@ export class DocumentListViewService { activeListViewState.currentPage = 1 this.reload() } else { + console.log(error) + this.selectionData = null let errorMessage if ( - typeof error.error !== 'string' && + typeof error.error === 'object' && Object.keys(error.error).length > 0 ) { // e.g. { archive_serial_number: Array } errorMessage = Object.keys(error.error) .map((fieldName) => { + const fieldNameBase = fieldName.split('__')[0] const fieldError: Array = error.error[fieldName] return `${ - this.sortFields.find((f) => f.field == fieldName)?.name + this.sortFields.find( + (f) => f.field?.split('__')[0] == fieldNameBase + )?.name ?? fieldNameBase }: ${fieldError[0]}` }) .join(', ') diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts index 72610abee..dd4df41f8 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -4,7 +4,8 @@ import { provideHttpClientTesting, } from '@angular/common/http/testing' import { TestBed } from '@angular/core/testing' -import { Subscription } from 'rxjs' +import { of, Subscription } from 'rxjs' +import { CustomFieldDataType } from 'src/app/data/custom-field' import { DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS_FULLTEXT, @@ -14,12 +15,15 @@ import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { environment } from 'src/environments/environment' import { PermissionsService } from '../permissions.service' import { SettingsService } from '../settings.service' +import { CustomFieldsService } from './custom-fields.service' import { DocumentService } from './document.service' let httpTestingController: HttpTestingController let service: DocumentService let subscription: Subscription let settingsService: SettingsService +let permissionsService: PermissionsService +let customFieldsService: CustomFieldsService const endpoint = 'documents' const documents = [ @@ -55,8 +59,29 @@ beforeEach(() => { }) httpTestingController = TestBed.inject(HttpTestingController) - service = TestBed.inject(DocumentService) settingsService = TestBed.inject(SettingsService) + customFieldsService = TestBed.inject(CustomFieldsService) + permissionsService = TestBed.inject(PermissionsService) + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + jest.spyOn(customFieldsService, 'listAll').mockReturnValue( + of({ + all: [1, 2, 3], + count: 3, + results: [ + { + id: 1, + name: 'Custom Field 1', + data_type: CustomFieldDataType.String, + }, + { + id: 2, + name: 'Custom Field 2', + data_type: CustomFieldDataType.Integer, + }, + ], + }) + ) + service = TestBed.inject(DocumentService) }) describe(`DocumentService`, () => { @@ -289,18 +314,25 @@ describe(`DocumentService`, () => { it('should construct sort fields respecting permissions', () => { expect( service.sortFields.find((f) => f.field === 'correspondent__name') - ).toBeUndefined() + ).not.toBeUndefined() expect( service.sortFields.find((f) => f.field === 'document_type__name') - ).toBeUndefined() + ).not.toBeUndefined() + expect( + service.sortFields.find((f) => f.field === 'owner') + ).not.toBeUndefined() - const permissionsService: PermissionsService = - TestBed.inject(PermissionsService) - jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false) service['setupSortFields']() - expect(service.sortFields).toEqual(DOCUMENT_SORT_FIELDS) + const fields = DOCUMENT_SORT_FIELDS.filter( + (f) => + ['correspondent__name', 'document_type__name', 'owner'].indexOf( + f.field + ) === -1 + ) + expect(service.sortFields).toEqual(fields) expect(service.sortFieldsFullText).toEqual([ - ...DOCUMENT_SORT_FIELDS, + ...fields, ...DOCUMENT_SORT_FIELDS_FULLTEXT, ]) @@ -311,6 +343,38 @@ it('should construct sort fields respecting permissions', () => { ).toBeUndefined() }) +it('should include custom fields in sort fields if user has permission', () => { + const permissionsService: PermissionsService = + TestBed.inject(PermissionsService) + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + + service['customFields'] = [ + { + id: 1, + name: 'Custom Field 1', + data_type: CustomFieldDataType.String, + }, + { + id: 2, + name: 'Custom Field 2', + data_type: CustomFieldDataType.Integer, + }, + ] + + service['setupSortFields']() + expect(service.sortFields).toEqual([ + ...DOCUMENT_SORT_FIELDS, + { + field: 'custom_field_1', + name: 'Custom Field 1', + }, + { + field: 'custom_field_2', + name: 'Custom Field 2', + }, + ]) +}) + afterEach(() => { subscription?.unsubscribe() httpTestingController.verify() diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index a703f0388..d9ae04563 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core' import { Observable } from 'rxjs' import { map, tap } from 'rxjs/operators' import { AuditLogEntry } from 'src/app/data/auditlog-entry' +import { CustomField } from 'src/app/data/custom-field' import { DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS_FULLTEXT, @@ -22,6 +23,7 @@ import { import { SettingsService } from '../settings.service' import { AbstractPaperlessService } from './abstract-paperless-service' import { CorrespondentService } from './correspondent.service' +import { CustomFieldsService } from './custom-fields.service' import { DocumentTypeService } from './document-type.service' import { StoragePathService } from './storage-path.service' import { TagService } from './tag.service' @@ -55,6 +57,8 @@ export class DocumentService extends AbstractPaperlessService { return this._sortFieldsFullText } + private customFields: CustomField[] = [] + constructor( http: HttpClient, private correspondentService: CorrespondentService, @@ -62,14 +66,40 @@ export class DocumentService extends AbstractPaperlessService { private tagService: TagService, private storagePathService: StoragePathService, private permissionsService: PermissionsService, - private settingsService: SettingsService + private settingsService: SettingsService, + private customFieldService: CustomFieldsService ) { super(http, 'documents') + if ( + this.permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.CustomField + ) + ) { + this.customFieldService.listAll().subscribe((fields) => { + this.customFields = fields.results + this.setupSortFields() + }) + } + this.setupSortFields() } private setupSortFields() { this._sortFields = [...DOCUMENT_SORT_FIELDS] + if ( + this.permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.CustomField + ) + ) { + this.customFields.forEach((field) => { + this._sortFields.push({ + field: `custom_field_${field.id}`, + name: field.name, + }) + }) + } let excludes = [] if ( !this.permissionsService.currentUserCan( diff --git a/src/documents/filters.py b/src/documents/filters.py index 237973b6f..185ba7b6f 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -6,10 +6,17 @@ from collections.abc import Callable from contextlib import contextmanager from django.contrib.contenttypes.models import ContentType +from django.db.models import Case from django.db.models import CharField from django.db.models import Count +from django.db.models import Exists +from django.db.models import IntegerField from django.db.models import OuterRef from django.db.models import Q +from django.db.models import Subquery +from django.db.models import Sum +from django.db.models import Value +from django.db.models import When from django.db.models.functions import Cast from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import BooleanFilter @@ -18,6 +25,7 @@ from django_filters.rest_framework import FilterSet from guardian.utils import get_group_obj_perms_model from guardian.utils import get_user_obj_perms_model from rest_framework import serializers +from rest_framework.filters import OrderingFilter from rest_framework_guardian.filters import ObjectPermissionsFilter from documents.models import Correspondent @@ -760,3 +768,141 @@ class ObjectOwnedPermissionsFilter(ObjectPermissionsFilter): objects_owned = queryset.filter(owner=request.user) objects_unowned = queryset.filter(owner__isnull=True) return objects_owned | objects_unowned + + +class DocumentsOrderingFilter(OrderingFilter): + field_name = "ordering" + prefix = "custom_field_" + + def filter_queryset(self, request, queryset, view): + param = request.query_params.get("ordering") + if param and self.prefix in param: + custom_field_id = int(param.split(self.prefix)[1]) + try: + field = CustomField.objects.get(pk=custom_field_id) + except CustomField.DoesNotExist: + raise serializers.ValidationError( + {self.prefix + str(custom_field_id): [_("Custom field not found")]}, + ) + + annotation = None + match field.data_type: + case CustomField.FieldDataType.STRING: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_text")[:1], + ) + case CustomField.FieldDataType.INT: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_int")[:1], + ) + case CustomField.FieldDataType.FLOAT: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_float")[:1], + ) + case CustomField.FieldDataType.DATE: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_date")[:1], + ) + case CustomField.FieldDataType.MONETARY: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_monetary_amount")[:1], + ) + case CustomField.FieldDataType.SELECT: + # Select options are a little more complicated since the value is the id of the option, not + # the label. Additionally, to support sqlite we can't use StringAgg, so we need to create a + # case statement for each option, setting the value to the index of the option in a list + # sorted by label, and then summing the results to give a single value for the annotation + + select_options = sorted( + field.extra_data.get("select_options", []), + key=lambda x: x.get("label"), + ) + whens = [ + When( + custom_fields__field_id=custom_field_id, + custom_fields__value_select=option.get("id"), + then=Value(idx, output_field=IntegerField()), + ) + for idx, option in enumerate(select_options) + ] + whens.append( + When( + custom_fields__field_id=custom_field_id, + custom_fields__value_select__isnull=True, + then=Value( + len(select_options), + output_field=IntegerField(), + ), + ), + ) + annotation = Sum( + Case( + *whens, + default=Value(0), + output_field=IntegerField(), + ), + ) + case CustomField.FieldDataType.DOCUMENTLINK: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_document_ids")[:1], + ) + case CustomField.FieldDataType.URL: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_url")[:1], + ) + case CustomField.FieldDataType.BOOL: + annotation = Subquery( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ).values("value_bool")[:1], + ) + + if not annotation: + # Only happens if a new data type is added and not handled here + raise ValueError("Invalid custom field data type") + + queryset = ( + queryset.annotate( + # We need to annotate the queryset with the custom field value + custom_field_value=annotation, + # We also need to annotate the queryset with a boolean for sorting whether the field exists + has_field=Exists( + CustomFieldInstance.objects.filter( + document_id=OuterRef("id"), + field_id=custom_field_id, + ), + ), + ) + .order_by( + "-has_field", + param.replace( + self.prefix + str(custom_field_id), + "custom_field_value", + ), + ) + .distinct() + ) + + return super().filter_queryset(request, queryset, view) diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 8307d6c4c..ea5227c8a 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -5,6 +5,7 @@ import tempfile import uuid import zoneinfo from binascii import hexlify +from datetime import date from datetime import timedelta from pathlib import Path from unittest import mock @@ -2762,3 +2763,184 @@ class TestDocumentApiV2(DirectoriesMixin, APITestCase): self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], "#000000", ) + + +class TestDocumentApiCustomFieldsSorting(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=self.user) + + self.doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + ) + self.doc2 = Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + ) + self.doc3 = Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + ) + + cache.clear() + + def test_document_custom_fields_sorting(self): + """ + GIVEN: + - Documents with custom fields + WHEN: + - API request for document filtering with custom field sorting + THEN: + - Documents are sorted by custom field values + """ + values = { + CustomField.FieldDataType.STRING: { + "values": ["foo", "bar", "baz"], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.STRING + ], + }, + CustomField.FieldDataType.INT: { + "values": [3, 1, 2], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.INT + ], + }, + CustomField.FieldDataType.FLOAT: { + "values": [3.3, 1.1, 2.2], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.FLOAT + ], + }, + CustomField.FieldDataType.BOOL: { + "values": [True, False, False], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.BOOL + ], + }, + CustomField.FieldDataType.DATE: { + "values": [date(2021, 1, 3), date(2021, 1, 1), date(2021, 1, 2)], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.DATE + ], + }, + CustomField.FieldDataType.URL: { + "values": [ + "http://example.org", + "http://example.com", + "http://example.net", + ], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.URL + ], + }, + CustomField.FieldDataType.MONETARY: { + "values": ["USD789.00", "USD123.00", "USD456.00"], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.MONETARY + ], + }, + CustomField.FieldDataType.DOCUMENTLINK: { + "values": [self.doc3.pk, self.doc1.pk, self.doc2.pk], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.DOCUMENTLINK + ], + }, + CustomField.FieldDataType.SELECT: { + "values": ["ghi-789", "abc-123", "def-456"], + "field_name": CustomFieldInstance.TYPE_TO_DATA_STORE_NAME_MAP[ + CustomField.FieldDataType.SELECT + ], + "extra_data": { + "select_options": [ + {"label": "Option 1", "id": "abc-123"}, + {"label": "Option 2", "id": "def-456"}, + {"label": "Option 3", "id": "ghi-789"}, + ], + }, + }, + } + + for data_type, data in values.items(): + CustomField.objects.all().delete() + CustomFieldInstance.objects.all().delete() + custom_field = CustomField.objects.create( + name=f"custom field {data_type}", + data_type=data_type, + extra_data=data.get("extra_data", {}), + ) + for i, value in enumerate(data["values"]): + CustomFieldInstance.objects.create( + document=[self.doc1, self.doc2, self.doc3][i], + field=custom_field, + **{data["field_name"]: value}, + ) + response = self.client.get( + f"/api/documents/?ordering=custom_field_{custom_field.pk}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [self.doc2.id, self.doc3.id, self.doc1.id], + ) + + response = self.client.get( + f"/api/documents/?ordering=-custom_field_{custom_field.pk}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + if data_type == CustomField.FieldDataType.BOOL: + # just check the first one for bools, as the rest are the same + self.assertEqual( + [results[0]["id"]], + [self.doc1.id], + ) + else: + self.assertEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [self.doc1.id, self.doc3.id, self.doc2.id], + ) + + def test_document_custom_fields_sorting_invalid(self): + """ + GIVEN: + - Documents with custom fields + WHEN: + - API request for document filtering with invalid custom field sorting + THEN: + - 400 is returned + """ + + response = self.client.get( + "/api/documents/?ordering=custom_field_999", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_document_custom_fields_sorting_invalid_data_type(self): + """ + GIVEN: + - Documents with custom fields + WHEN: + - API request for document filtering with a custom field sorting with a new (unhandled) data type + THEN: + - Error is raised + """ + + custom_field = CustomField.objects.create( + name="custom field", + data_type="foo", + ) + + with self.assertRaises(ValueError): + self.client.get( + f"/api/documents/?ordering=custom_field_{custom_field.pk}", + ) diff --git a/src/documents/views.py b/src/documents/views.py index 6d2c8cbd8..4e2e4a8bf 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -96,6 +96,7 @@ from documents.data_models import DocumentSource from documents.filters import CorrespondentFilterSet from documents.filters import CustomFieldFilterSet from documents.filters import DocumentFilterSet +from documents.filters import DocumentsOrderingFilter from documents.filters import DocumentTypeFilterSet from documents.filters import ObjectOwnedOrGrantedPermissionsFilter from documents.filters import ObjectOwnedPermissionsFilter @@ -350,7 +351,7 @@ class DocumentViewSet( filter_backends = ( DjangoFilterBackend, SearchFilter, - OrderingFilter, + DocumentsOrderingFilter, ObjectOwnedOrGrantedPermissionsFilter, ) filterset_class = DocumentFilterSet @@ -367,6 +368,7 @@ class DocumentViewSet( "num_notes", "owner", "page_count", + "custom_field_", ) def get_queryset(self):