Custom field filtering

This commit is contained in:
shamoon 2024-04-23 22:04:14 -07:00
parent bd4476d484
commit 1ba7ec3cb9
13 changed files with 521 additions and 11 deletions

View File

@ -80,7 +80,7 @@ django_checks() {
search_index() {
local -r index_version=8
local -r index_version=9
local -r index_version_file=${DATA_DIR}/.index_version
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then

View File

@ -68,6 +68,10 @@ const selectionData: SelectionData = {
{ id: 66, document_count: 3 },
{ id: 55, document_count: 0 },
],
selected_custom_fields: [
{ id: 1, document_count: 3 },
{ id: 2, document_count: 0 },
],
}
describe('BulkEditorComponent', () => {

View File

@ -70,6 +70,18 @@
[documentCounts]="storagePathDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
<pngx-filterable-dropdown class="flex-fill" title="Custom fields" icon="ui-radios" i18n-title
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
[items]="customFields"
[manyToOne]="true"
[(selectionModel)]="customFieldSelectionModel"
(selectionModelChange)="updateRules()"
(opened)="onCustomFieldsDropdownOpen()"
[documentCounts]="customFieldDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown>
}
</div>
<div class="d-flex flex-wrap gap-2">
<pngx-date-dropdown

View File

@ -49,8 +49,12 @@ import {
FILTER_OWNER_ANY,
FILTER_OWNER_DOES_NOT_INCLUDE,
FILTER_OWNER_ISNULL,
FILTER_CUSTOM_FIELDS,
FILTER_CUSTOM_FIELDS_TEXT,
FILTER_SHARED_BY_USER,
FILTER_HAS_CUSTOM_FIELDS_ANY,
FILTER_HAS_ANY_CUSTOM_FIELDS,
FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
FILTER_HAS_CUSTOM_FIELDS_ALL,
} from 'src/app/data/filter-rule-type'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
@ -86,6 +90,8 @@ import {
PermissionsService,
} from 'src/app/services/permissions.service'
import { environment } from 'src/environments/environment'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
const tags: Tag[] = [
{
@ -131,6 +137,19 @@ const storage_paths: StoragePath[] = [
},
]
const custom_fields: CustomField[] = [
{
id: 42,
data_type: CustomFieldDataType.String,
name: 'CustomField42',
},
{
id: 43,
data_type: CustomFieldDataType.String,
name: 'CustomField43',
},
]
const users: User[] = [
{
id: 1,
@ -187,6 +206,12 @@ describe('FilterEditorComponent', () => {
listAll: () => of({ results: storage_paths }),
},
},
{
provide: CustomFieldsService,
useValue: {
listAll: () => of({ results: custom_fields }),
},
},
{
provide: UserService,
useValue: {
@ -285,7 +310,7 @@ describe('FilterEditorComponent', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
rule_type: FILTER_CUSTOM_FIELDS,
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
value: 'foo',
},
]
@ -806,6 +831,110 @@ describe('FilterEditorComponent', () => {
]
}))
it('should ingest filter rules for has all custom fields', fakeAsync(() => {
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
0
)
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: '42',
},
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: '43',
},
]
expect(component.customFieldSelectionModel.logicalOperator).toEqual(
LogicalOperator.And
)
expect(component.customFieldSelectionModel.getSelectedItems()).toEqual(
custom_fields
)
// coverage
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: null,
},
]
component.toggleTag(2) // coverage
}))
it('should ingest filter rules for has any custom fields', fakeAsync(() => {
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
0
)
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
value: '42',
},
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
value: '43',
},
]
expect(component.customFieldSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.customFieldSelectionModel.getSelectedItems()).toEqual(
custom_fields
)
// coverage
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
value: null,
},
]
}))
it('should ingest filter rules for has any custom field', fakeAsync(() => {
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
0
)
component.filterRules = [
{
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
value: '1',
},
]
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
1
)
expect(component.customFieldSelectionModel.get(null)).toBeTruthy()
}))
it('should ingest filter rules for exclude tag(s)', fakeAsync(() => {
expect(component.customFieldSelectionModel.getExcludedItems()).toHaveLength(
0
)
component.filterRules = [
{
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
value: '42',
},
{
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
value: '43',
},
]
expect(component.customFieldSelectionModel.logicalOperator).toEqual(
LogicalOperator.And
)
expect(component.customFieldSelectionModel.getExcludedItems()).toEqual(
custom_fields
)
// coverage
component.filterRules = [
{
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
value: null,
},
]
}))
it('should ingest filter rules for owner', fakeAsync(() => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
@ -1053,7 +1182,7 @@ describe('FilterEditorComponent', () => {
expect(component.textFilterTarget).toEqual('custom-fields')
expect(component.filterRules).toEqual([
{
rule_type: FILTER_CUSTOM_FIELDS,
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
value: 'foo',
},
])
@ -1317,6 +1446,75 @@ describe('FilterEditorComponent', () => {
])
}))
it('should convert user input to correct filter rules on custom field select not assigned', fakeAsync(() => {
const customFieldsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[4]
customFieldsFilterableDropdown.triggerEventHandler('opened')
const customFieldButton = customFieldsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)[0]
customFieldButton.triggerEventHandler('toggle')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
value: 'false',
},
])
}))
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
const customFieldsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[4] // CF dropdown
customFieldsFilterableDropdown.triggerEventHandler('opened')
const customFieldButtons = customFieldsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)
customFieldButtons[1].triggerEventHandler('toggle')
customFieldButtons[2].triggerEventHandler('toggle')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: custom_fields[0].id.toString(),
},
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: custom_fields[1].id.toString(),
},
])
const toggleOperatorButtons = customFieldsFilterableDropdown.queryAll(
By.css('input[type=radio]')
)
toggleOperatorButtons[1].nativeElement.checked = true
toggleOperatorButtons[1].triggerEventHandler('change')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
value: custom_fields[0].id.toString(),
},
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
value: custom_fields[1].id.toString(),
},
])
customFieldButtons[2].triggerEventHandler('exclude')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: custom_fields[0].id.toString(),
},
{
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
value: custom_fields[1].id.toString(),
},
])
}))
it('should convert user input to correct filter rules on date created after', fakeAsync(() => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DateDropdownComponent)
@ -1645,6 +1843,10 @@ describe('FilterEditorComponent', () => {
{ id: 32, document_count: 1 },
{ id: 33, document_count: 0 },
],
selected_custom_fields: [
{ id: 42, document_count: 1 },
{ id: 43, document_count: 0 },
],
}
})
@ -1719,6 +1921,24 @@ describe('FilterEditorComponent', () => {
]
expect(component.generateFilterName()).toEqual('Without any tag')
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
value: '42',
},
]
expect(component.generateFilterName()).toEqual(
`Custom fields: ${custom_fields[0].name}`
)
component.filterRules = [
{
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
value: 'false',
},
]
expect(component.generateFilterName()).toEqual('Without any custom field')
component.filterRules = [
{
rule_type: FILTER_TITLE,

View File

@ -48,8 +48,12 @@ import {
FILTER_OWNER_DOES_NOT_INCLUDE,
FILTER_OWNER_ISNULL,
FILTER_OWNER_ANY,
FILTER_CUSTOM_FIELDS,
FILTER_CUSTOM_FIELDS_TEXT,
FILTER_SHARED_BY_USER,
FILTER_HAS_CUSTOM_FIELDS_ANY,
FILTER_HAS_CUSTOM_FIELDS_ALL,
FILTER_HAS_ANY_CUSTOM_FIELDS,
FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
} from 'src/app/data/filter-rule-type'
import {
FilterableDropdownSelectionModel,
@ -76,6 +80,8 @@ import {
PermissionsService,
} from 'src/app/services/permissions.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { CustomField } from 'src/app/data/custom-field'
const TEXT_FILTER_TARGET_TITLE = 'title'
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
@ -208,6 +214,16 @@ export class FilterEditorComponent
return $localize`Without any tag`
}
case FILTER_HAS_CUSTOM_FIELDS_ALL:
return $localize`Custom fields: ${this.customFields.find(
(f) => f.id == +rule.value
)?.name}`
case FILTER_HAS_ANY_CUSTOM_FIELDS:
if (rule.value == 'false') {
return $localize`Without any custom field`
}
case FILTER_TITLE:
return $localize`Title: ${rule.value}`
@ -234,7 +250,8 @@ export class FilterEditorComponent
private correspondentService: CorrespondentService,
private documentService: DocumentService,
private storagePathService: StoragePathService,
public permissionsService: PermissionsService
public permissionsService: PermissionsService,
private customFieldService: CustomFieldsService
) {
super()
}
@ -246,11 +263,13 @@ export class FilterEditorComponent
correspondents: Correspondent[] = []
documentTypes: DocumentType[] = []
storagePaths: StoragePath[] = []
customFields: CustomField[] = []
tagDocumentCounts: SelectionDataItem[]
correspondentDocumentCounts: SelectionDataItem[]
documentTypeDocumentCounts: SelectionDataItem[]
storagePathDocumentCounts: SelectionDataItem[]
customFieldDocumentCounts: SelectionDataItem[]
_textFilter = ''
_moreLikeId: number
@ -288,6 +307,7 @@ export class FilterEditorComponent
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathSelectionModel = new FilterableDropdownSelectionModel()
customFieldSelectionModel = new FilterableDropdownSelectionModel()
dateCreatedBefore: string
dateCreatedAfter: string
@ -322,6 +342,7 @@ export class FilterEditorComponent
this.storagePathSelectionModel.clear(false)
this.tagSelectionModel.clear(false)
this.correspondentSelectionModel.clear(false)
this.customFieldSelectionModel.clear(false)
this._textFilter = null
this._moreLikeId = null
this.dateAddedBefore = null
@ -347,7 +368,7 @@ export class FilterEditorComponent
this._textFilter = rule.value
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
break
case FILTER_CUSTOM_FIELDS:
case FILTER_CUSTOM_FIELDS_TEXT:
this._textFilter = rule.value
this.textFilterTarget = TEXT_FILTER_TARGET_CUSTOM_FIELDS
break
@ -488,6 +509,36 @@ export class FilterEditorComponent
false
)
break
case FILTER_HAS_CUSTOM_FIELDS_ALL:
this.customFieldSelectionModel.logicalOperator = LogicalOperator.And
this.customFieldSelectionModel.set(
rule.value ? +rule.value : null,
ToggleableItemState.Selected,
false
)
break
case FILTER_HAS_CUSTOM_FIELDS_ANY:
this.customFieldSelectionModel.logicalOperator = LogicalOperator.Or
this.customFieldSelectionModel.set(
rule.value ? +rule.value : null,
ToggleableItemState.Selected,
false
)
break
case FILTER_HAS_ANY_CUSTOM_FIELDS:
this.customFieldSelectionModel.set(
null,
ToggleableItemState.Selected,
false
)
break
case FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS:
this.customFieldSelectionModel.set(
rule.value ? +rule.value : null,
ToggleableItemState.Excluded,
false
)
break
case FILTER_ASN_ISNULL:
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
this.textFilterModifier =
@ -595,7 +646,7 @@ export class FilterEditorComponent
this.textFilterTarget == TEXT_FILTER_TARGET_CUSTOM_FIELDS
) {
filterRules.push({
rule_type: FILTER_CUSTOM_FIELDS,
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
value: this._textFilter,
})
}
@ -703,6 +754,35 @@ export class FilterEditorComponent
})
})
}
if (this.customFieldSelectionModel.isNoneSelected()) {
filterRules.push({
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
value: 'false',
})
} else {
const customFieldFilterType =
this.customFieldSelectionModel.logicalOperator == LogicalOperator.And
? FILTER_HAS_CUSTOM_FIELDS_ALL
: FILTER_HAS_CUSTOM_FIELDS_ANY
this.customFieldSelectionModel
.getSelectedItems()
.filter((field) => field.id)
.forEach((field) => {
filterRules.push({
rule_type: customFieldFilterType,
value: field.id?.toString(),
})
})
this.customFieldSelectionModel
.getExcludedItems()
.filter((field) => field.id)
.forEach((field) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
value: field.id?.toString(),
})
})
}
if (this.dateCreatedBefore) {
filterRules.push({
rule_type: FILTER_CREATED_BEFORE,
@ -845,6 +925,8 @@ export class FilterEditorComponent
selectionData?.selected_correspondents ?? null
this.storagePathDocumentCounts =
selectionData?.selected_storage_paths ?? null
this.customFieldDocumentCounts =
selectionData?.selected_custom_fields ?? null
}
rulesModified: boolean = false
@ -905,6 +987,16 @@ export class FilterEditorComponent
.listAll()
.subscribe((result) => (this.storagePaths = result.results))
}
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.CustomField
)
) {
this.customFieldService
.listAll()
.subscribe((result) => (this.customFields = result.results))
}
this.textFilterDebounce = new Subject<string>()
@ -961,6 +1053,10 @@ export class FilterEditorComponent
this.storagePathSelectionModel.apply()
}
onCustomFieldsDropdownOpen() {
this.customFieldSelectionModel.apply()
}
updateTextFilter(text) {
this._textFilter = text
this.documentService.searchQuery = text

View File

@ -47,7 +47,11 @@ export const FILTER_OWNER_ISNULL = 34
export const FILTER_OWNER_DOES_NOT_INCLUDE = 35
export const FILTER_SHARED_BY_USER = 37
export const FILTER_CUSTOM_FIELDS = 36
export const FILTER_CUSTOM_FIELDS_TEXT = 36
export const FILTER_HAS_CUSTOM_FIELDS_ALL = 38
export const FILTER_HAS_CUSTOM_FIELDS_ANY = 39
export const FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS = 40
export const FILTER_HAS_ANY_CUSTOM_FIELDS = 41
export const FILTER_RULE_TYPES: FilterRuleType[] = [
{
@ -281,11 +285,36 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
multi: true,
},
{
id: FILTER_CUSTOM_FIELDS,
id: FILTER_CUSTOM_FIELDS_TEXT,
filtervar: 'custom_fields__icontains',
datatype: 'string',
multi: false,
},
{
id: FILTER_HAS_CUSTOM_FIELDS_ALL,
filtervar: 'custom_fields__id__all',
datatype: 'number',
multi: true,
},
{
id: FILTER_HAS_CUSTOM_FIELDS_ANY,
filtervar: 'custom_fields__id__in',
datatype: 'number',
multi: true,
},
{
id: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
filtervar: 'custom_fields__id__none',
datatype: 'number',
multi: true,
},
{
id: FILTER_HAS_ANY_CUSTOM_FIELDS,
filtervar: 'has_custom_fields',
datatype: 'boolean',
multi: false,
default: true,
},
]
export interface FilterRuleType {

View File

@ -36,6 +36,7 @@ export interface SelectionData {
selected_correspondents: SelectionDataItem[]
selected_tags: SelectionDataItem[]
selected_document_types: SelectionDataItem[]
selected_custom_fields: SelectionDataItem[]
}
@Injectable({

View File

@ -199,6 +199,25 @@ class DocumentFilterSet(FilterSet):
custom_fields__icontains = CustomFieldsFilter()
custom_fields__id__all = ObjectFilter(field_name="custom_fields__field")
custom_fields__id__none = ObjectFilter(
field_name="custom_fields__field",
exclude=True,
)
custom_fields__id__in = ObjectFilter(
field_name="custom_fields__field",
in_list=True,
)
has_custom_fields = BooleanFilter(
label="Has custom field",
field_name="custom_fields",
lookup_expr="isnull",
exclude=True,
)
shared_by__id = SharedByUser()
class Meta:

View File

@ -70,6 +70,8 @@ def get_schema():
num_notes=NUMERIC(sortable=True, signed=False),
custom_fields=TEXT(),
custom_field_count=NUMERIC(sortable=True, signed=False),
has_custom_fields=BOOLEAN(),
custom_fields_id=KEYWORD(commas=True),
owner=TEXT(),
owner_id=NUMERIC(),
has_owner=BOOLEAN(),
@ -125,6 +127,9 @@ def update_document(writer: AsyncWriter, doc: Document):
custom_fields = ",".join(
[str(c) for c in CustomFieldInstance.objects.filter(document=doc)],
)
custom_fields_ids = ",".join(
[str(f.field.id) for f in CustomFieldInstance.objects.filter(document=doc)],
)
asn = doc.archive_serial_number
if asn is not None and (
asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
@ -166,6 +171,8 @@ def update_document(writer: AsyncWriter, doc: Document):
num_notes=len(notes),
custom_fields=custom_fields,
custom_field_count=len(doc.custom_fields.all()),
has_custom_fields=len(custom_fields) > 0,
custom_fields_id=custom_fields_ids if custom_fields_ids else None,
owner=doc.owner.username if doc.owner else None,
owner_id=doc.owner.id if doc.owner else None,
has_owner=doc.owner is not None,
@ -206,7 +213,10 @@ class DelayedQuery:
"created": ("created", ["date__lt", "date__gt"]),
"checksum": ("checksum", ["icontains", "istartswith"]),
"original_filename": ("original_filename", ["icontains", "istartswith"]),
"custom_fields": ("custom_fields", ["icontains", "istartswith"]),
"custom_fields": (
"custom_fields",
["icontains", "istartswith", "id__all", "id__in", "id__none"],
),
}
def _get_query(self):
@ -220,6 +230,12 @@ class DelayedQuery:
criterias.append(query.Term("has_tag", self.evalBoolean(value)))
continue
if key == "has_custom_fields":
criterias.append(
query.Term("has_custom_fields", self.evalBoolean(value)),
)
continue
# Don't process query params without a filter
if "__" not in key:
continue

View File

@ -0,0 +1,65 @@
# Generated by Django 4.2.11 on 2024-04-24 04:58
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1046_workflowaction_remove_all_correspondents_and_more"),
]
operations = [
migrations.AlterField(
model_name="savedviewfilterrule",
name="rule_type",
field=models.PositiveIntegerField(
choices=[
(0, "title contains"),
(1, "content contains"),
(2, "ASN is"),
(3, "correspondent is"),
(4, "document type is"),
(5, "is in inbox"),
(6, "has tag"),
(7, "has any tag"),
(8, "created before"),
(9, "created after"),
(10, "created year is"),
(11, "created month is"),
(12, "created day is"),
(13, "added before"),
(14, "added after"),
(15, "modified before"),
(16, "modified after"),
(17, "does not have tag"),
(18, "does not have ASN"),
(19, "title or content contains"),
(20, "fulltext query"),
(21, "more like this"),
(22, "has tags in"),
(23, "ASN greater than"),
(24, "ASN less than"),
(25, "storage path is"),
(26, "has correspondent in"),
(27, "does not have correspondent in"),
(28, "has document type in"),
(29, "does not have document type in"),
(30, "has storage path in"),
(31, "does not have storage path in"),
(32, "owner is"),
(33, "has owner in"),
(34, "does not have owner"),
(35, "does not have owner in"),
(36, "has custom field value"),
(37, "is shared by me"),
(38, "has custom fields"),
(39, "has custom field in"),
(40, "does not have custom field in"),
(41, "does not have custom field"),
],
verbose_name="rule type",
),
),
]

View File

@ -500,6 +500,10 @@ class SavedViewFilterRule(models.Model):
(35, _("does not have owner in")),
(36, _("has custom field value")),
(37, _("is shared by me")),
(38, _("has custom fields")),
(39, _("has custom field in")),
(40, _("does not have custom field in")),
(41, _("does not have custom field")),
]
saved_view = models.ForeignKey(

View File

@ -920,6 +920,34 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
),
)
self.assertIn(
d4.id,
search_query(
"&has_custom_fields=1",
),
)
self.assertIn(
d4.id,
search_query(
"&custom_fields__id__in=" + str(cf1.id),
),
)
self.assertIn(
d4.id,
search_query(
"&custom_fields__id__all=" + str(cf1.id),
),
)
self.assertNotIn(
d4.id,
search_query(
"&custom_fields__id__none=" + str(cf1.id),
),
)
def test_search_filtering_respect_owner(self):
"""
GIVEN:

View File

@ -1065,6 +1065,18 @@ class SelectionDataView(GenericAPIView):
),
)
custom_fields = CustomField.objects.annotate(
document_count=Count(
Case(
When(
fields__document__id__in=ids,
then=1,
),
output_field=IntegerField(),
),
),
)
r = Response(
{
"selected_correspondents": [
@ -1081,6 +1093,10 @@ class SelectionDataView(GenericAPIView):
{"id": t.id, "document_count": t.document_count}
for t in storage_paths
],
"selected_custom_fields": [
{"id": t.id, "document_count": t.document_count}
for t in custom_fields
],
},
)