Bulk edit custom fields
This commit is contained in:
parent
b399790681
commit
7241c85f95
@ -4,7 +4,7 @@
|
|||||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||||
<div class="row d-flex">
|
<div class="row d-flex">
|
||||||
<div class="col border-end">
|
<div class="col border-end">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
|
@ -74,6 +74,20 @@
|
|||||||
(apply)="setStoragePaths($event)">
|
(apply)="setStoragePaths($event)">
|
||||||
</pngx-filterable-dropdown>
|
</pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||||
|
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
|
||||||
|
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
|
||||||
|
[items]="customFields"
|
||||||
|
[disabled]="!userCanEditAll"
|
||||||
|
[editing]="true"
|
||||||
|
[applyOnClose]="applyOnClose"
|
||||||
|
[createRef]="createCustomField.bind(this)"
|
||||||
|
(opened)="openCustomFieldsDropdown()"
|
||||||
|
[(selectionModel)]="customFieldsSelectionModel"
|
||||||
|
[documentCounts]="customFieldDocumentCounts"
|
||||||
|
(apply)="setCustomFields($event)">
|
||||||
|
</pngx-filterable-dropdown>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||||
<div class="btn-toolbar">
|
<div class="btn-toolbar">
|
||||||
|
@ -55,6 +55,9 @@ import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage
|
|||||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
|
||||||
const selectionData: SelectionData = {
|
const selectionData: SelectionData = {
|
||||||
selected_tags: [
|
selected_tags: [
|
||||||
@ -69,8 +72,8 @@ const selectionData: SelectionData = {
|
|||||||
{ id: 55, document_count: 0 },
|
{ id: 55, document_count: 0 },
|
||||||
],
|
],
|
||||||
selected_custom_fields: [
|
selected_custom_fields: [
|
||||||
{ id: 1, document_count: 3 },
|
{ id: 77, document_count: 3 },
|
||||||
{ id: 2, document_count: 0 },
|
{ id: 88, document_count: 0 },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +89,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
let correspondentsService: CorrespondentService
|
let correspondentsService: CorrespondentService
|
||||||
let documentTypeService: DocumentTypeService
|
let documentTypeService: DocumentTypeService
|
||||||
let storagePathService: StoragePathService
|
let storagePathService: StoragePathService
|
||||||
|
let customFieldsService: CustomFieldsService
|
||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -152,6 +156,18 @@ describe('BulkEditorComponent', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CustomFieldsService,
|
||||||
|
useValue: {
|
||||||
|
listAll: () =>
|
||||||
|
of({
|
||||||
|
results: [
|
||||||
|
{ id: 77, name: 'customfield1' },
|
||||||
|
{ id: 88, name: 'customfield2' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
{
|
{
|
||||||
@ -193,6 +209,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
correspondentsService = TestBed.inject(CorrespondentService)
|
correspondentsService = TestBed.inject(CorrespondentService)
|
||||||
documentTypeService = TestBed.inject(DocumentTypeService)
|
documentTypeService = TestBed.inject(DocumentTypeService)
|
||||||
storagePathService = TestBed.inject(StoragePathService)
|
storagePathService = TestBed.inject(StoragePathService)
|
||||||
|
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||||
httpTestingController = TestBed.inject(HttpTestingController)
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
|
|
||||||
fixture = TestBed.createComponent(BulkEditorComponent)
|
fixture = TestBed.createComponent(BulkEditorComponent)
|
||||||
@ -266,6 +283,22 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
|
expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should apply selection data to custom fields menu', () => {
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(
|
||||||
|
component.customFieldsSelectionModel.getSelectedItems()
|
||||||
|
).toHaveLength(0)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 5, 7]))
|
||||||
|
jest
|
||||||
|
.spyOn(documentService, 'getSelectionData')
|
||||||
|
.mockReturnValue(of(selectionData))
|
||||||
|
component.openCustomFieldsDropdown()
|
||||||
|
expect(component.customFieldsSelectionModel.selectionSize()).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should execute modify tags bulk operation', () => {
|
it('should execute modify tags bulk operation', () => {
|
||||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
jest
|
jest
|
||||||
@ -683,6 +716,122 @@ describe('BulkEditorComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should execute modify custom fields bulk operation', () => {
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = false
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [{ id: 101 }],
|
||||||
|
itemsToRemove: [{ id: 102 }],
|
||||||
|
})
|
||||||
|
let req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
|
)
|
||||||
|
req.flush(true)
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
documents: [3, 4],
|
||||||
|
method: 'modify_custom_fields',
|
||||||
|
parameters: { add_custom_fields: [101], remove_custom_fields: [102] },
|
||||||
|
})
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
) // list reload
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
|
) // listAllFilteredIds
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute modify custom fields bulk operation with confirmation dialog if enabled', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = true
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [{ id: 101 }],
|
||||||
|
itemsToRemove: [{ id: 102 }],
|
||||||
|
})
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
modal.componentInstance.confirm()
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
|
.flush(true)
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
) // list reload
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
|
) // listAllFilteredIds
|
||||||
|
|
||||||
|
// coverage for modal messages
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [{ id: 101 }],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [{ id: 101 }, { id: 102 }],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [],
|
||||||
|
itemsToRemove: [{ id: 101 }, { id: 102 }],
|
||||||
|
})
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [{ id: 100 }],
|
||||||
|
itemsToRemove: [{ id: 101 }, { id: 102 }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set modal dialog text accordingly for custom fields edit confirmation', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = true
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [],
|
||||||
|
itemsToRemove: [{ id: 101, name: 'CustomField 101' }],
|
||||||
|
})
|
||||||
|
expect(modal.componentInstance.message).toEqual(
|
||||||
|
'This operation will remove the custom field "CustomField 101" from 2 selected document(s).'
|
||||||
|
)
|
||||||
|
modal.close()
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [{ id: 101, name: 'CustomField 101' }],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
|
expect(modal.componentInstance.message).toEqual(
|
||||||
|
'This operation will assign the custom field "CustomField 101" to 2 selected document(s).'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should only execute bulk operations when changes are detected', () => {
|
it('should only execute bulk operations when changes are detected', () => {
|
||||||
component.setTags({
|
component.setTags({
|
||||||
itemsToAdd: [],
|
itemsToAdd: [],
|
||||||
@ -700,6 +849,10 @@ describe('BulkEditorComponent', () => {
|
|||||||
itemsToAdd: [],
|
itemsToAdd: [],
|
||||||
itemsToRemove: [],
|
itemsToRemove: [],
|
||||||
})
|
})
|
||||||
|
component.setCustomFields({
|
||||||
|
itemsToAdd: [],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
httpTestingController.expectNone(
|
httpTestingController.expectNone(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
@ -1183,4 +1336,56 @@ describe('BulkEditorComponent', () => {
|
|||||||
)
|
)
|
||||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support create new custom field', () => {
|
||||||
|
const name = 'New Custom Field'
|
||||||
|
const newCustomField = { id: 101, name: 'New Custom Field' }
|
||||||
|
const customFields: Results<CustomField> = {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Custom Field 1',
|
||||||
|
data_type: CustomFieldDataType.String,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Custom Field 2',
|
||||||
|
data_type: CustomFieldDataType.String,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
all: [1, 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalInstance = {
|
||||||
|
componentInstance: {
|
||||||
|
dialogMode: EditDialogMode.CREATE,
|
||||||
|
object: { name },
|
||||||
|
succeeded: of(newCustomField),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const customFieldsListAllSpy = jest.spyOn(customFieldsService, 'listAll')
|
||||||
|
customFieldsListAllSpy.mockReturnValue(of(customFields))
|
||||||
|
|
||||||
|
const customFieldsSelectionModelToggleSpy = jest.spyOn(
|
||||||
|
component.customFieldsSelectionModel,
|
||||||
|
'toggle'
|
||||||
|
)
|
||||||
|
|
||||||
|
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||||
|
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||||
|
|
||||||
|
component.createCustomField(name)
|
||||||
|
|
||||||
|
expect(modalServiceOpenSpy).toHaveBeenCalledWith(
|
||||||
|
CustomFieldEditDialogComponent,
|
||||||
|
{ backdrop: 'static' }
|
||||||
|
)
|
||||||
|
expect(customFieldsListAllSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||||
|
newCustomField.id
|
||||||
|
)
|
||||||
|
expect(component.customFields).toEqual(customFields.results)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -41,6 +41,9 @@ import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/docume
|
|||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||||
|
import { CustomField } from 'src/app/data/custom-field'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-bulk-editor',
|
selector: 'pngx-bulk-editor',
|
||||||
@ -55,15 +58,18 @@ export class BulkEditorComponent
|
|||||||
correspondents: Correspondent[]
|
correspondents: Correspondent[]
|
||||||
documentTypes: DocumentType[]
|
documentTypes: DocumentType[]
|
||||||
storagePaths: StoragePath[]
|
storagePaths: StoragePath[]
|
||||||
|
customFields: CustomField[]
|
||||||
|
|
||||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
customFieldsSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
correspondentDocumentCounts: SelectionDataItem[]
|
correspondentDocumentCounts: SelectionDataItem[]
|
||||||
documentTypeDocumentCounts: SelectionDataItem[]
|
documentTypeDocumentCounts: SelectionDataItem[]
|
||||||
storagePathDocumentCounts: SelectionDataItem[]
|
storagePathDocumentCounts: SelectionDataItem[]
|
||||||
|
customFieldDocumentCounts: SelectionDataItem[]
|
||||||
awaitingDownload: boolean
|
awaitingDownload: boolean
|
||||||
|
|
||||||
unsubscribeNotifier: Subject<any> = new Subject()
|
unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
@ -85,6 +91,7 @@ export class BulkEditorComponent
|
|||||||
private settings: SettingsService,
|
private settings: SettingsService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
|
private customFieldService: CustomFieldsService,
|
||||||
private permissionService: PermissionsService
|
private permissionService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@ -166,6 +173,17 @@ export class BulkEditorComponent
|
|||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((result) => (this.storagePaths = result.results))
|
.subscribe((result) => (this.storagePaths = result.results))
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.permissionService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.CustomField
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.customFieldService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.customFields = result.results))
|
||||||
|
}
|
||||||
|
|
||||||
this.downloadForm
|
this.downloadForm
|
||||||
.get('downloadFileTypeArchive')
|
.get('downloadFileTypeArchive')
|
||||||
@ -297,6 +315,19 @@ export class BulkEditorComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openCustomFieldsDropdown() {
|
||||||
|
this.documentService
|
||||||
|
.getSelectionData(Array.from(this.list.selected))
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((s) => {
|
||||||
|
this.customFieldDocumentCounts = s.selected_custom_fields
|
||||||
|
this.applySelectionData(
|
||||||
|
s.selected_custom_fields,
|
||||||
|
this.customFieldsSelectionModel
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private _localizeList(items: MatchingModel[]) {
|
private _localizeList(items: MatchingModel[]) {
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
return ''
|
return ''
|
||||||
@ -495,6 +526,74 @@ export class BulkEditorComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomFields(changedCustomFields: ChangedItems) {
|
||||||
|
if (
|
||||||
|
changedCustomFields.itemsToAdd.length == 0 &&
|
||||||
|
changedCustomFields.itemsToRemove.length == 0
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (this.showConfirmationDialogs) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.title = $localize`Confirm custom field assignment`
|
||||||
|
if (
|
||||||
|
changedCustomFields.itemsToAdd.length == 1 &&
|
||||||
|
changedCustomFields.itemsToRemove.length == 0
|
||||||
|
) {
|
||||||
|
let customField = changedCustomFields.itemsToAdd[0]
|
||||||
|
modal.componentInstance.message = $localize`This operation will assign the custom field "${customField.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else if (
|
||||||
|
changedCustomFields.itemsToAdd.length > 1 &&
|
||||||
|
changedCustomFields.itemsToRemove.length == 0
|
||||||
|
) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will assign the custom fields ${this._localizeList(
|
||||||
|
changedCustomFields.itemsToAdd
|
||||||
|
)} to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else if (
|
||||||
|
changedCustomFields.itemsToAdd.length == 0 &&
|
||||||
|
changedCustomFields.itemsToRemove.length == 1
|
||||||
|
) {
|
||||||
|
let customField = changedCustomFields.itemsToRemove[0]
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the custom field "${customField.name}" from ${this.list.selected.size} selected document(s).`
|
||||||
|
} else if (
|
||||||
|
changedCustomFields.itemsToAdd.length == 0 &&
|
||||||
|
changedCustomFields.itemsToRemove.length > 1
|
||||||
|
) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the custom fields ${this._localizeList(
|
||||||
|
changedCustomFields.itemsToRemove
|
||||||
|
)} from ${this.list.selected.size} selected document(s).`
|
||||||
|
} else {
|
||||||
|
modal.componentInstance.message = $localize`This operation will assign the custom fields ${this._localizeList(
|
||||||
|
changedCustomFields.itemsToAdd
|
||||||
|
)} and remove the custom fields ${this._localizeList(
|
||||||
|
changedCustomFields.itemsToRemove
|
||||||
|
)} on ${this.list.selected.size} selected document(s).`
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.executeBulkOperation(modal, 'modify_custom_fields', {
|
||||||
|
add_custom_fields: changedCustomFields.itemsToAdd.map((f) => f.id),
|
||||||
|
remove_custom_fields: changedCustomFields.itemsToRemove.map(
|
||||||
|
(f) => f.id
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.executeBulkOperation(null, 'modify_custom_fields', {
|
||||||
|
add_custom_fields: changedCustomFields.itemsToAdd.map((f) => f.id),
|
||||||
|
remove_custom_fields: changedCustomFields.itemsToRemove.map(
|
||||||
|
(f) => f.id
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createTag(name: string) {
|
createTag(name: string) {
|
||||||
let modal = this.modalService.open(TagEditDialogComponent, {
|
let modal = this.modalService.open(TagEditDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
@ -581,6 +680,27 @@ export class BulkEditorComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createCustomField(name: string) {
|
||||||
|
let modal = this.modalService.open(CustomFieldEditDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||||
|
modal.componentInstance.object = { name }
|
||||||
|
modal.componentInstance.succeeded
|
||||||
|
.pipe(
|
||||||
|
switchMap((newCustomField) => {
|
||||||
|
return this.customFieldService
|
||||||
|
.listAll()
|
||||||
|
.pipe(map((customFields) => ({ newCustomField, customFields })))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(({ newCustomField, customFields }) => {
|
||||||
|
this.customFields = customFields.results
|
||||||
|
this.customFieldsSelectionModel.toggle(newCustomField.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
applyDelete() {
|
applyDelete() {
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
|
@ -82,8 +82,6 @@
|
|||||||
[documentCounts]="customFieldDocumentCounts"
|
[documentCounts]="customFieldDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
<pngx-dates-dropdown
|
<pngx-dates-dropdown
|
||||||
title="Dates" i18n-title
|
title="Dates" i18n-title
|
||||||
(datesSet)="updateRules()"
|
(datesSet)="updateRules()"
|
||||||
@ -94,8 +92,6 @@
|
|||||||
[(addedDateAfter)]="dateAddedAfter"
|
[(addedDateAfter)]="dateAddedAfter"
|
||||||
[(addedRelativeDate)]="dateAddedRelativeDate">
|
[(addedRelativeDate)]="dateAddedRelativeDate">
|
||||||
</pngx-dates-dropdown>
|
</pngx-dates-dropdown>
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-wrap">
|
|
||||||
<pngx-permissions-filter-dropdown
|
<pngx-permissions-filter-dropdown
|
||||||
title="Permissions" i18n-title
|
title="Permissions" i18n-title
|
||||||
(ownerFilterSet)="updateRules()"
|
(ownerFilterSet)="updateRules()"
|
||||||
|
@ -12,6 +12,7 @@ from documents.data_models import ConsumableDocument
|
|||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
@ -120,6 +121,30 @@ def modify_tags(doc_ids, add_tags, remove_tags):
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def modify_custom_fields(doc_ids, add_custom_fields, remove_custom_fields):
|
||||||
|
qs = Document.objects.filter(id__in=doc_ids)
|
||||||
|
affected_docs = [doc.id for doc in qs]
|
||||||
|
|
||||||
|
fields_to_add = []
|
||||||
|
for field in add_custom_fields:
|
||||||
|
for doc_id in affected_docs:
|
||||||
|
fields_to_add.append(
|
||||||
|
CustomFieldInstance(
|
||||||
|
document_id=doc_id,
|
||||||
|
field_id=field,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CustomFieldInstance.objects.bulk_create(fields_to_add)
|
||||||
|
CustomFieldInstance.objects.filter(
|
||||||
|
document_id__in=affected_docs,
|
||||||
|
field_id__in=remove_custom_fields,
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
bulk_update_documents.delay(document_ids=affected_docs)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
def delete(doc_ids):
|
def delete(doc_ids):
|
||||||
Document.objects.filter(id__in=doc_ids).delete()
|
Document.objects.filter(id__in=doc_ids).delete()
|
||||||
|
|
||||||
|
@ -905,6 +905,7 @@ class BulkEditSerializer(
|
|||||||
"add_tag",
|
"add_tag",
|
||||||
"remove_tag",
|
"remove_tag",
|
||||||
"modify_tags",
|
"modify_tags",
|
||||||
|
"modify_custom_fields",
|
||||||
"delete",
|
"delete",
|
||||||
"redo_ocr",
|
"redo_ocr",
|
||||||
"set_permissions",
|
"set_permissions",
|
||||||
@ -929,6 +930,17 @@ class BulkEditSerializer(
|
|||||||
f"Some tags in {name} don't exist or were specified twice.",
|
f"Some tags in {name} don't exist or were specified twice.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _validate_custom_field_id_list(self, custom_fields, name="custom_fields"):
|
||||||
|
if not isinstance(custom_fields, list):
|
||||||
|
raise serializers.ValidationError(f"{name} must be a list")
|
||||||
|
if not all(isinstance(i, int) for i in custom_fields):
|
||||||
|
raise serializers.ValidationError(f"{name} must be a list of integers")
|
||||||
|
count = CustomField.objects.filter(id__in=custom_fields).count()
|
||||||
|
if not count == len(custom_fields):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Some custom fields in {name} don't exist or were specified twice.",
|
||||||
|
)
|
||||||
|
|
||||||
def validate_method(self, method):
|
def validate_method(self, method):
|
||||||
if method == "set_correspondent":
|
if method == "set_correspondent":
|
||||||
return bulk_edit.set_correspondent
|
return bulk_edit.set_correspondent
|
||||||
@ -942,6 +954,8 @@ class BulkEditSerializer(
|
|||||||
return bulk_edit.remove_tag
|
return bulk_edit.remove_tag
|
||||||
elif method == "modify_tags":
|
elif method == "modify_tags":
|
||||||
return bulk_edit.modify_tags
|
return bulk_edit.modify_tags
|
||||||
|
elif method == "modify_custom_fields":
|
||||||
|
return bulk_edit.modify_custom_fields
|
||||||
elif method == "delete":
|
elif method == "delete":
|
||||||
return bulk_edit.delete
|
return bulk_edit.delete
|
||||||
elif method == "redo_ocr":
|
elif method == "redo_ocr":
|
||||||
@ -1017,6 +1031,23 @@ class BulkEditSerializer(
|
|||||||
else:
|
else:
|
||||||
raise serializers.ValidationError("remove_tags not specified")
|
raise serializers.ValidationError("remove_tags not specified")
|
||||||
|
|
||||||
|
def _validate_parameters_modify_custom_fields(self, parameters):
|
||||||
|
if "add_custom_fields" in parameters:
|
||||||
|
self._validate_custom_field_id_list(
|
||||||
|
parameters["add_custom_fields"],
|
||||||
|
"add_custom_fields",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError("add_custom_fields not specified")
|
||||||
|
|
||||||
|
if "remove_custom_fields" in parameters:
|
||||||
|
self._validate_custom_field_id_list(
|
||||||
|
parameters["remove_custom_fields"],
|
||||||
|
"remove_custom_fields",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError("remove_custom_fields not specified")
|
||||||
|
|
||||||
def _validate_owner(self, owner):
|
def _validate_owner(self, owner):
|
||||||
ownerUser = User.objects.get(pk=owner)
|
ownerUser = User.objects.get(pk=owner)
|
||||||
if ownerUser is None:
|
if ownerUser is None:
|
||||||
@ -1079,6 +1110,8 @@ class BulkEditSerializer(
|
|||||||
self._validate_parameters_modify_tags(parameters)
|
self._validate_parameters_modify_tags(parameters)
|
||||||
elif method == bulk_edit.set_storage_path:
|
elif method == bulk_edit.set_storage_path:
|
||||||
self._validate_storage_path(parameters)
|
self._validate_storage_path(parameters)
|
||||||
|
elif method == bulk_edit.modify_custom_fields:
|
||||||
|
self._validate_parameters_modify_custom_fields(parameters)
|
||||||
elif method == bulk_edit.set_permissions:
|
elif method == bulk_edit.set_permissions:
|
||||||
self._validate_parameters_set_permissions(parameters)
|
self._validate_parameters_set_permissions(parameters)
|
||||||
elif method == bulk_edit.rotate:
|
elif method == bulk_edit.rotate:
|
||||||
|
@ -7,6 +7,7 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
|
from documents.models import CustomField
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
@ -49,6 +50,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.doc3.tags.add(self.t2)
|
self.doc3.tags.add(self.t2)
|
||||||
self.doc4.tags.add(self.t1, self.t2)
|
self.doc4.tags.add(self.t1, self.t2)
|
||||||
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
|
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
|
||||||
|
self.cf1 = CustomField.objects.create(name="cf1", data_type="text")
|
||||||
|
self.cf2 = CustomField.objects.create(name="cf2", data_type="text")
|
||||||
|
|
||||||
@mock.patch("documents.serialisers.bulk_edit.set_correspondent")
|
@mock.patch("documents.serialisers.bulk_edit.set_correspondent")
|
||||||
def test_api_set_correspondent(self, m):
|
def test_api_set_correspondent(self, m):
|
||||||
@ -222,6 +225,135 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.modify_custom_fields")
|
||||||
|
def test_api_modify_custom_fields(self, m):
|
||||||
|
m.return_value = "OK"
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc1.id, self.doc3.id],
|
||||||
|
"method": "modify_custom_fields",
|
||||||
|
"parameters": {
|
||||||
|
"add_custom_fields": [self.cf1.id],
|
||||||
|
"remove_custom_fields": [self.cf2.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
m.assert_called_once()
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
self.assertListEqual(args[0], [self.doc1.id, self.doc3.id])
|
||||||
|
self.assertEqual(kwargs["add_custom_fields"], [self.cf1.id])
|
||||||
|
self.assertEqual(kwargs["remove_custom_fields"], [self.cf2.id])
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.modify_custom_fields")
|
||||||
|
def test_api_modify_custom_fields_invalid_params(self, m):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- API data to modify custom fields is malformed
|
||||||
|
WHEN:
|
||||||
|
- API to edit custom fields is called
|
||||||
|
THEN:
|
||||||
|
- API returns HTTP 400
|
||||||
|
- modify_custom_fields is not called
|
||||||
|
"""
|
||||||
|
m.return_value = "OK"
|
||||||
|
|
||||||
|
# Missing add_custom_fields
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc1.id, self.doc3.id],
|
||||||
|
"method": "modify_custom_fields",
|
||||||
|
"parameters": {
|
||||||
|
"add_custom_fields": [self.cf1.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
# Missing remove_custom_fields
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc1.id, self.doc3.id],
|
||||||
|
"method": "modify_custom_fields",
|
||||||
|
"parameters": {
|
||||||
|
"remove_custom_fields": [self.cf1.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
# Not a list
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc1.id, self.doc3.id],
|
||||||
|
"method": "modify_custom_fields",
|
||||||
|
"parameters": {
|
||||||
|
"add_custom_fields": self.cf1.id,
|
||||||
|
"remove_custom_fields": self.cf2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
# Not a list of integers
|
||||||
|
|
||||||
|
# Missing remove_custom_fields
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc1.id, self.doc3.id],
|
||||||
|
"method": "modify_custom_fields",
|
||||||
|
"parameters": {
|
||||||
|
"add_custom_fields": ["foo"],
|
||||||
|
"remove_custom_fields": ["bar"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
# Custom field ID not found
|
||||||
|
|
||||||
|
# Missing remove_custom_fields
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc1.id, self.doc3.id],
|
||||||
|
"method": "modify_custom_fields",
|
||||||
|
"parameters": {
|
||||||
|
"add_custom_fields": [self.cf1.id],
|
||||||
|
"remove_custom_fields": [99],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
@mock.patch("documents.serialisers.bulk_edit.delete")
|
@mock.patch("documents.serialisers.bulk_edit.delete")
|
||||||
def test_api_delete(self, m):
|
def test_api_delete(self, m):
|
||||||
m.return_value = "OK"
|
m.return_value = "OK"
|
||||||
|
@ -11,6 +11,8 @@ from guardian.shortcuts import get_users_with_perms
|
|||||||
|
|
||||||
from documents import bulk_edit
|
from documents import bulk_edit
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
|
from documents.models import CustomField
|
||||||
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
@ -186,6 +188,53 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
|
|||||||
# TODO: doc3 should not be affected, but the query for that is rather complicated
|
# TODO: doc3 should not be affected, but the query for that is rather complicated
|
||||||
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
|
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
|
||||||
|
|
||||||
|
def test_modify_custom_fields(self):
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name="cf1",
|
||||||
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
|
)
|
||||||
|
cf2 = CustomField.objects.create(
|
||||||
|
name="cf2",
|
||||||
|
data_type=CustomField.FieldDataType.INT,
|
||||||
|
)
|
||||||
|
cf3 = CustomField.objects.create(
|
||||||
|
name="cf3",
|
||||||
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
|
)
|
||||||
|
CustomFieldInstance.objects.create(
|
||||||
|
document=self.doc1,
|
||||||
|
field=cf,
|
||||||
|
)
|
||||||
|
CustomFieldInstance.objects.create(
|
||||||
|
document=self.doc2,
|
||||||
|
field=cf,
|
||||||
|
)
|
||||||
|
CustomFieldInstance.objects.create(
|
||||||
|
document=self.doc2,
|
||||||
|
field=cf3,
|
||||||
|
)
|
||||||
|
bulk_edit.modify_custom_fields(
|
||||||
|
[self.doc1.id, self.doc2.id],
|
||||||
|
add_custom_fields=[cf2.id],
|
||||||
|
remove_custom_fields=[cf.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.doc1.refresh_from_db()
|
||||||
|
self.doc2.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.doc1.custom_fields.count(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.doc2.custom_fields.count(),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.async_task.assert_called_once()
|
||||||
|
args, kwargs = self.async_task.call_args
|
||||||
|
self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
self.assertEqual(Document.objects.count(), 5)
|
self.assertEqual(Document.objects.count(), 5)
|
||||||
bulk_edit.delete([self.doc1.id, self.doc2.id])
|
bulk_edit.delete([self.doc1.id, self.doc2.id])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user