Enhancement: custom field sorting (#8494)

This commit is contained in:
shamoon 2024-12-30 10:18:34 -08:00 committed by GitHub
parent e44cfef662
commit 4e3d25c714
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 522 additions and 25 deletions

View File

@ -5942,7 +5942,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">289</context>
<context context-type="linenumber">294</context>
</context-group>
</trans-unit>
<trans-unit id="78870852467682010" datatype="html">
@ -5957,7 +5957,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">329</context>
<context context-type="linenumber">334</context>
</context-group>
</trans-unit>
<trans-unit id="157572966557284263" datatype="html">
@ -5972,7 +5972,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">336</context>
<context context-type="linenumber">341</context>
</context-group>
</trans-unit>
<trans-unit id="872092479747931526" datatype="html">
@ -7209,7 +7209,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">305</context>
<context context-type="linenumber">310</context>
</context-group>
</trans-unit>
<trans-unit id="106713086593101376" datatype="html">
@ -7573,25 +7573,32 @@
<context context-type="linenumber">261,263</context>
</context-group>
</trans-unit>
<trans-unit id="5083658411133224968" datatype="html">
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">268,269</context>
</context-group>
</trans-unit>
<trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">297</context>
<context context-type="linenumber">302</context>
</context-group>
</trans-unit>
<trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">298</context>
<context context-type="linenumber">303</context>
</context-group>
</trans-unit>
<trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">357</context>
<context context-type="linenumber">362</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
@ -7602,7 +7609,7 @@
<source>No</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">357</context>
<context context-type="linenumber">362</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>

View File

@ -262,9 +262,14 @@
Shared
</th>
}
@for (field of activeDisplayCustomFields; track field) {
<th>
{{getDisplayCustomFieldTitle(field)}}
@for (field_id of activeDisplayCustomFields; track field_id) {
<th class="cursor-pointer"
pngxSortable="{{field_id}}"
title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)">
{{getDisplayCustomFieldTitle(field_id)}}
</th>
}
</tr>

View File

@ -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(),

View File

@ -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()

View File

@ -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<string> }
errorMessage = Object.keys(error.error)
.map((fieldName) => {
const fieldNameBase = fieldName.split('__')[0]
const fieldError: Array<string> = 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(', ')

View File

@ -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()

View File

@ -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<Document> {
return this._sortFieldsFullText
}
private customFields: CustomField[] = []
constructor(
http: HttpClient,
private correspondentService: CorrespondentService,
@ -62,14 +66,40 @@ export class DocumentService extends AbstractPaperlessService<Document> {
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(

View File

@ -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)

View File

@ -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}",
)

View File

@ -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):