Custom field filtering
This commit is contained in:
parent
bd4476d484
commit
1ba7ec3cb9
@ -80,7 +80,7 @@ django_checks() {
|
|||||||
|
|
||||||
search_index() {
|
search_index() {
|
||||||
|
|
||||||
local -r index_version=8
|
local -r index_version=9
|
||||||
local -r index_version_file=${DATA_DIR}/.index_version
|
local -r index_version_file=${DATA_DIR}/.index_version
|
||||||
|
|
||||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||||
|
@ -68,6 +68,10 @@ const selectionData: SelectionData = {
|
|||||||
{ id: 66, document_count: 3 },
|
{ id: 66, document_count: 3 },
|
||||||
{ id: 55, document_count: 0 },
|
{ id: 55, document_count: 0 },
|
||||||
],
|
],
|
||||||
|
selected_custom_fields: [
|
||||||
|
{ id: 1, document_count: 3 },
|
||||||
|
{ id: 2, document_count: 0 },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('BulkEditorComponent', () => {
|
describe('BulkEditorComponent', () => {
|
||||||
|
@ -70,6 +70,18 @@
|
|||||||
[documentCounts]="storagePathDocumentCounts"
|
[documentCounts]="storagePathDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[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>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<pngx-date-dropdown
|
<pngx-date-dropdown
|
||||||
|
@ -49,8 +49,12 @@ import {
|
|||||||
FILTER_OWNER_ANY,
|
FILTER_OWNER_ANY,
|
||||||
FILTER_OWNER_DOES_NOT_INCLUDE,
|
FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||||
FILTER_OWNER_ISNULL,
|
FILTER_OWNER_ISNULL,
|
||||||
FILTER_CUSTOM_FIELDS,
|
FILTER_CUSTOM_FIELDS_TEXT,
|
||||||
FILTER_SHARED_BY_USER,
|
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'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
@ -86,6 +90,8 @@ import {
|
|||||||
PermissionsService,
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { environment } from 'src/environments/environment'
|
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[] = [
|
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[] = [
|
const users: User[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -187,6 +206,12 @@ describe('FilterEditorComponent', () => {
|
|||||||
listAll: () => of({ results: storage_paths }),
|
listAll: () => of({ results: storage_paths }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CustomFieldsService,
|
||||||
|
useValue: {
|
||||||
|
listAll: () => of({ results: custom_fields }),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@ -285,7 +310,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
expect(component.textFilter).toEqual(null)
|
expect(component.textFilter).toEqual(null)
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
{
|
{
|
||||||
rule_type: FILTER_CUSTOM_FIELDS,
|
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
|
||||||
value: 'foo',
|
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(() => {
|
it('should ingest filter rules for owner', fakeAsync(() => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
@ -1053,7 +1182,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
expect(component.textFilterTarget).toEqual('custom-fields')
|
expect(component.textFilterTarget).toEqual('custom-fields')
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
rule_type: FILTER_CUSTOM_FIELDS,
|
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
|
||||||
value: 'foo',
|
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(() => {
|
it('should convert user input to correct filter rules on date created after', fakeAsync(() => {
|
||||||
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
||||||
By.directive(DateDropdownComponent)
|
By.directive(DateDropdownComponent)
|
||||||
@ -1645,6 +1843,10 @@ describe('FilterEditorComponent', () => {
|
|||||||
{ id: 32, document_count: 1 },
|
{ id: 32, document_count: 1 },
|
||||||
{ id: 33, document_count: 0 },
|
{ 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')
|
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 = [
|
component.filterRules = [
|
||||||
{
|
{
|
||||||
rule_type: FILTER_TITLE,
|
rule_type: FILTER_TITLE,
|
||||||
|
@ -48,8 +48,12 @@ import {
|
|||||||
FILTER_OWNER_DOES_NOT_INCLUDE,
|
FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||||
FILTER_OWNER_ISNULL,
|
FILTER_OWNER_ISNULL,
|
||||||
FILTER_OWNER_ANY,
|
FILTER_OWNER_ANY,
|
||||||
FILTER_CUSTOM_FIELDS,
|
FILTER_CUSTOM_FIELDS_TEXT,
|
||||||
FILTER_SHARED_BY_USER,
|
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'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import {
|
import {
|
||||||
FilterableDropdownSelectionModel,
|
FilterableDropdownSelectionModel,
|
||||||
@ -76,6 +80,8 @@ import {
|
|||||||
PermissionsService,
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
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 = 'title'
|
||||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||||
@ -208,6 +214,16 @@ export class FilterEditorComponent
|
|||||||
return $localize`Without any tag`
|
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:
|
case FILTER_TITLE:
|
||||||
return $localize`Title: ${rule.value}`
|
return $localize`Title: ${rule.value}`
|
||||||
|
|
||||||
@ -234,7 +250,8 @@ export class FilterEditorComponent
|
|||||||
private correspondentService: CorrespondentService,
|
private correspondentService: CorrespondentService,
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
public permissionsService: PermissionsService
|
public permissionsService: PermissionsService,
|
||||||
|
private customFieldService: CustomFieldsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -246,11 +263,13 @@ export class FilterEditorComponent
|
|||||||
correspondents: Correspondent[] = []
|
correspondents: Correspondent[] = []
|
||||||
documentTypes: DocumentType[] = []
|
documentTypes: DocumentType[] = []
|
||||||
storagePaths: StoragePath[] = []
|
storagePaths: StoragePath[] = []
|
||||||
|
customFields: CustomField[] = []
|
||||||
|
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
correspondentDocumentCounts: SelectionDataItem[]
|
correspondentDocumentCounts: SelectionDataItem[]
|
||||||
documentTypeDocumentCounts: SelectionDataItem[]
|
documentTypeDocumentCounts: SelectionDataItem[]
|
||||||
storagePathDocumentCounts: SelectionDataItem[]
|
storagePathDocumentCounts: SelectionDataItem[]
|
||||||
|
customFieldDocumentCounts: SelectionDataItem[]
|
||||||
|
|
||||||
_textFilter = ''
|
_textFilter = ''
|
||||||
_moreLikeId: number
|
_moreLikeId: number
|
||||||
@ -288,6 +307,7 @@ export class FilterEditorComponent
|
|||||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
customFieldSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
dateCreatedBefore: string
|
dateCreatedBefore: string
|
||||||
dateCreatedAfter: string
|
dateCreatedAfter: string
|
||||||
@ -322,6 +342,7 @@ export class FilterEditorComponent
|
|||||||
this.storagePathSelectionModel.clear(false)
|
this.storagePathSelectionModel.clear(false)
|
||||||
this.tagSelectionModel.clear(false)
|
this.tagSelectionModel.clear(false)
|
||||||
this.correspondentSelectionModel.clear(false)
|
this.correspondentSelectionModel.clear(false)
|
||||||
|
this.customFieldSelectionModel.clear(false)
|
||||||
this._textFilter = null
|
this._textFilter = null
|
||||||
this._moreLikeId = null
|
this._moreLikeId = null
|
||||||
this.dateAddedBefore = null
|
this.dateAddedBefore = null
|
||||||
@ -347,7 +368,7 @@ export class FilterEditorComponent
|
|||||||
this._textFilter = rule.value
|
this._textFilter = rule.value
|
||||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
break
|
break
|
||||||
case FILTER_CUSTOM_FIELDS:
|
case FILTER_CUSTOM_FIELDS_TEXT:
|
||||||
this._textFilter = rule.value
|
this._textFilter = rule.value
|
||||||
this.textFilterTarget = TEXT_FILTER_TARGET_CUSTOM_FIELDS
|
this.textFilterTarget = TEXT_FILTER_TARGET_CUSTOM_FIELDS
|
||||||
break
|
break
|
||||||
@ -488,6 +509,36 @@ export class FilterEditorComponent
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
break
|
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:
|
case FILTER_ASN_ISNULL:
|
||||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
this.textFilterModifier =
|
this.textFilterModifier =
|
||||||
@ -595,7 +646,7 @@ export class FilterEditorComponent
|
|||||||
this.textFilterTarget == TEXT_FILTER_TARGET_CUSTOM_FIELDS
|
this.textFilterTarget == TEXT_FILTER_TARGET_CUSTOM_FIELDS
|
||||||
) {
|
) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_CUSTOM_FIELDS,
|
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
|
||||||
value: this._textFilter,
|
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) {
|
if (this.dateCreatedBefore) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_CREATED_BEFORE,
|
rule_type: FILTER_CREATED_BEFORE,
|
||||||
@ -845,6 +925,8 @@ export class FilterEditorComponent
|
|||||||
selectionData?.selected_correspondents ?? null
|
selectionData?.selected_correspondents ?? null
|
||||||
this.storagePathDocumentCounts =
|
this.storagePathDocumentCounts =
|
||||||
selectionData?.selected_storage_paths ?? null
|
selectionData?.selected_storage_paths ?? null
|
||||||
|
this.customFieldDocumentCounts =
|
||||||
|
selectionData?.selected_custom_fields ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesModified: boolean = false
|
rulesModified: boolean = false
|
||||||
@ -905,6 +987,16 @@ export class FilterEditorComponent
|
|||||||
.listAll()
|
.listAll()
|
||||||
.subscribe((result) => (this.storagePaths = result.results))
|
.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>()
|
this.textFilterDebounce = new Subject<string>()
|
||||||
|
|
||||||
@ -961,6 +1053,10 @@ export class FilterEditorComponent
|
|||||||
this.storagePathSelectionModel.apply()
|
this.storagePathSelectionModel.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCustomFieldsDropdownOpen() {
|
||||||
|
this.customFieldSelectionModel.apply()
|
||||||
|
}
|
||||||
|
|
||||||
updateTextFilter(text) {
|
updateTextFilter(text) {
|
||||||
this._textFilter = text
|
this._textFilter = text
|
||||||
this.documentService.searchQuery = text
|
this.documentService.searchQuery = text
|
||||||
|
@ -47,7 +47,11 @@ export const FILTER_OWNER_ISNULL = 34
|
|||||||
export const FILTER_OWNER_DOES_NOT_INCLUDE = 35
|
export const FILTER_OWNER_DOES_NOT_INCLUDE = 35
|
||||||
export const FILTER_SHARED_BY_USER = 37
|
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[] = [
|
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||||
{
|
{
|
||||||
@ -281,11 +285,36 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
|||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: FILTER_CUSTOM_FIELDS,
|
id: FILTER_CUSTOM_FIELDS_TEXT,
|
||||||
filtervar: 'custom_fields__icontains',
|
filtervar: 'custom_fields__icontains',
|
||||||
datatype: 'string',
|
datatype: 'string',
|
||||||
multi: false,
|
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 {
|
export interface FilterRuleType {
|
||||||
|
@ -36,6 +36,7 @@ export interface SelectionData {
|
|||||||
selected_correspondents: SelectionDataItem[]
|
selected_correspondents: SelectionDataItem[]
|
||||||
selected_tags: SelectionDataItem[]
|
selected_tags: SelectionDataItem[]
|
||||||
selected_document_types: SelectionDataItem[]
|
selected_document_types: SelectionDataItem[]
|
||||||
|
selected_custom_fields: SelectionDataItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -199,6 +199,25 @@ class DocumentFilterSet(FilterSet):
|
|||||||
|
|
||||||
custom_fields__icontains = CustomFieldsFilter()
|
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()
|
shared_by__id = SharedByUser()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -70,6 +70,8 @@ def get_schema():
|
|||||||
num_notes=NUMERIC(sortable=True, signed=False),
|
num_notes=NUMERIC(sortable=True, signed=False),
|
||||||
custom_fields=TEXT(),
|
custom_fields=TEXT(),
|
||||||
custom_field_count=NUMERIC(sortable=True, signed=False),
|
custom_field_count=NUMERIC(sortable=True, signed=False),
|
||||||
|
has_custom_fields=BOOLEAN(),
|
||||||
|
custom_fields_id=KEYWORD(commas=True),
|
||||||
owner=TEXT(),
|
owner=TEXT(),
|
||||||
owner_id=NUMERIC(),
|
owner_id=NUMERIC(),
|
||||||
has_owner=BOOLEAN(),
|
has_owner=BOOLEAN(),
|
||||||
@ -125,6 +127,9 @@ def update_document(writer: AsyncWriter, doc: Document):
|
|||||||
custom_fields = ",".join(
|
custom_fields = ",".join(
|
||||||
[str(c) for c in CustomFieldInstance.objects.filter(document=doc)],
|
[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
|
asn = doc.archive_serial_number
|
||||||
if asn is not None and (
|
if asn is not None and (
|
||||||
asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||||
@ -166,6 +171,8 @@ def update_document(writer: AsyncWriter, doc: Document):
|
|||||||
num_notes=len(notes),
|
num_notes=len(notes),
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
custom_field_count=len(doc.custom_fields.all()),
|
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=doc.owner.username if doc.owner else None,
|
||||||
owner_id=doc.owner.id if doc.owner else None,
|
owner_id=doc.owner.id if doc.owner else None,
|
||||||
has_owner=doc.owner is not None,
|
has_owner=doc.owner is not None,
|
||||||
@ -206,7 +213,10 @@ class DelayedQuery:
|
|||||||
"created": ("created", ["date__lt", "date__gt"]),
|
"created": ("created", ["date__lt", "date__gt"]),
|
||||||
"checksum": ("checksum", ["icontains", "istartswith"]),
|
"checksum": ("checksum", ["icontains", "istartswith"]),
|
||||||
"original_filename": ("original_filename", ["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):
|
def _get_query(self):
|
||||||
@ -220,6 +230,12 @@ class DelayedQuery:
|
|||||||
criterias.append(query.Term("has_tag", self.evalBoolean(value)))
|
criterias.append(query.Term("has_tag", self.evalBoolean(value)))
|
||||||
continue
|
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
|
# Don't process query params without a filter
|
||||||
if "__" not in key:
|
if "__" not in key:
|
||||||
continue
|
continue
|
||||||
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -500,6 +500,10 @@ class SavedViewFilterRule(models.Model):
|
|||||||
(35, _("does not have owner in")),
|
(35, _("does not have owner in")),
|
||||||
(36, _("has custom field value")),
|
(36, _("has custom field value")),
|
||||||
(37, _("is shared by me")),
|
(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(
|
saved_view = models.ForeignKey(
|
||||||
|
@ -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):
|
def test_search_filtering_respect_owner(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
@ -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(
|
r = Response(
|
||||||
{
|
{
|
||||||
"selected_correspondents": [
|
"selected_correspondents": [
|
||||||
@ -1081,6 +1093,10 @@ class SelectionDataView(GenericAPIView):
|
|||||||
{"id": t.id, "document_count": t.document_count}
|
{"id": t.id, "document_count": t.document_count}
|
||||||
for t in storage_paths
|
for t in storage_paths
|
||||||
],
|
],
|
||||||
|
"selected_custom_fields": [
|
||||||
|
{"id": t.id, "document_count": t.document_count}
|
||||||
|
for t in custom_fields
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user