Frontend support for bulk object permissions edit

This commit is contained in:
shamoon 2023-09-13 01:02:44 -07:00
parent db4b9c09e3
commit 60a76b02e6
10 changed files with 205 additions and 35 deletions

View File

@ -57,7 +57,8 @@ export class ToastsComponent implements OnInit, OnDestroy {
} }
getErrorText(error: any) { getErrorText(error: any) {
const text: string = error.error?.detail ?? error.error ?? '' let text: string = error.error?.detail ?? error.error ?? ''
if (typeof text === 'object') text = JSON.stringify(text)
return `${text.slice(0, 200)}${text.length > 200 ? '...' : ''}` return `${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`
} }
} }

View File

@ -1,4 +1,14 @@
<pngx-page-header title="{{ typeNamePlural | titlecase }}"> <pngx-page-header title="{{ typeNamePlural | titlecase }}">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-primary me-5" (click)="setPermissions()" [disabled]="!userOwnsAll || selectedObjects.size === 0">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button> <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button>
</pngx-page-header> </pngx-page-header>
@ -16,6 +26,12 @@
<table class="table table-striped align-middle border shadow-sm"> <table class="table table-striped align-middle border shadow-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-objects"></label>
</div>
</th>
<th scope="col" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> <th scope="col" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" class="d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th> <th scope="col" class="d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th> <th scope="col" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
@ -30,7 +46,13 @@
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
</td> </td>
</tr> </tr>
<tr *ngFor="let object of data"> <tr *ngFor="let object of data" (click)="toggleSelected(object, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
</div>
</td>
<td scope="row">{{ object.name }}</td> <td scope="row">{{ object.name }}</td>
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
<td scope="row">{{ object.document_count }}</td> <td scope="row">{{ object.document_count }}</td>
@ -54,17 +76,17 @@
</div> </div>
</div> </div>
<div class="btn-group d-none d-sm-block"> <div class="btn-group d-none d-sm-block">
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" /> <use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>&nbsp;<ng-container i18n>Documents</ng-container> </svg>&nbsp;<ng-container i18n>Documents</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)"> <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container> </svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)"> <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
@ -75,7 +97,10 @@
</tbody> </tbody>
</table> </table>
<div class="d-flex" *ngIf="!isLoading"> <div class="d-flex mb-2" *ngIf="!isLoading">
<div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div> <div *ngIf="collectionSize > 0">
<ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
<ng-container *ngIf="selectedObjects.size > 0">&nbsp;({{selectedObjects.size}} selected)</ng-container>
</div>
<ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination> <ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div> </div>

View File

@ -35,6 +35,7 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { MATCH_AUTO } from 'src/app/data/matching-model' import { MATCH_AUTO } from 'src/app/data/matching-model'
import { MATCH_NONE } from 'src/app/data/matching-model' import { MATCH_NONE } from 'src/app/data/matching-model'
import { MATCH_LITERAL } from 'src/app/data/matching-model' import { MATCH_LITERAL } from 'src/app/data/matching-model'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
const tags: PaperlessTag[] = [ const tags: PaperlessTag[] = [
{ {
@ -72,6 +73,7 @@ describe('ManagementListComponent', () => {
IfPermissionsDirective, IfPermissionsDirective,
SafeHtmlPipe, SafeHtmlPipe,
ConfirmDialogComponent, ConfirmDialogComponent,
PermissionsDialogComponent,
], ],
providers: [ providers: [
{ {
@ -145,7 +147,7 @@ describe('ManagementListComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData') const reloadSpy = jest.spyOn(component, 'reloadData')
const createButton = fixture.debugElement.queryAll(By.css('button'))[0] const createButton = fixture.debugElement.queryAll(By.css('button'))[2]
createButton.triggerEventHandler('click') createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined() expect(modal).not.toBeUndefined()
@ -170,7 +172,7 @@ describe('ManagementListComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData') const reloadSpy = jest.spyOn(component, 'reloadData')
const editButton = fixture.debugElement.queryAll(By.css('button'))[3] const editButton = fixture.debugElement.queryAll(By.css('button'))[5]
editButton.triggerEventHandler('click') editButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined() expect(modal).not.toBeUndefined()
@ -196,7 +198,7 @@ describe('ManagementListComponent', () => {
const deleteSpy = jest.spyOn(tagService, 'delete') const deleteSpy = jest.spyOn(tagService, 'delete')
const reloadSpy = jest.spyOn(component, 'reloadData') const reloadSpy = jest.spyOn(component, 'reloadData')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4] const deleteButton = fixture.debugElement.queryAll(By.css('button'))[6]
deleteButton.triggerEventHandler('click') deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined() expect(modal).not.toBeUndefined()
@ -216,7 +218,7 @@ describe('ManagementListComponent', () => {
it('should support quick filter for objects', () => { it('should support quick filter for objects', () => {
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
const filterButton = fixture.debugElement.queryAll(By.css('button'))[2] const filterButton = fixture.debugElement.queryAll(By.css('button'))[4]
filterButton.triggerEventHandler('click') filterButton.triggerEventHandler('click')
expect(qfSpy).toHaveBeenCalledWith([ expect(qfSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() }, { rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
@ -229,4 +231,47 @@ describe('ManagementListComponent', () => {
sortable.triggerEventHandler('click') sortable.triggerEventHandler('click')
expect(reloadSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled()
}) })
it('should support toggle all items in view', () => {
expect(component.selectedObjects.size).toEqual(0)
const toggleAllSpy = jest.spyOn(component, 'toggleAll')
const checkButton = fixture.debugElement.queryAll(
By.css('input.form-check-input')
)[0]
checkButton.nativeElement.dispatchEvent(new Event('click'))
checkButton.nativeElement.checked = true
checkButton.nativeElement.dispatchEvent(new Event('click'))
expect(toggleAllSpy).toHaveBeenCalled()
expect(component.selectedObjects.size).toEqual(tags.length)
})
it('should support bulk edit permissions', () => {
const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_update_permissions')
component.toggleSelected(tags[0])
component.toggleSelected(tags[1])
component.toggleSelected(tags[2])
component.toggleSelected(tags[2]) // uncheck, for coverage
const selected = new Set([tags[0].id, tags[1].id])
expect(component.selectedObjects).toEqual(selected)
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
fixture.detectChanges()
component.setPermissions()
expect(modal).not.toBeUndefined()
// fail first
bulkEditPermsSpy.mockReturnValueOnce(
throwError(() => new Error('error setting permissions'))
)
const errorToastSpy = jest.spyOn(toastService, 'showError')
modal.componentInstance.confirmClicked.emit()
expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(errorToastSpy).toHaveBeenCalled()
const successToastSpy = jest.spyOn(toastService, 'showInfo')
bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
modal.componentInstance.confirmClicked.emit()
expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(successToastSpy).toHaveBeenCalled()
})
}) })

View File

@ -6,7 +6,7 @@ import {
ViewChildren, ViewChildren,
} from '@angular/core' } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, Subscription } from 'rxjs' import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators' import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'
import { import {
MatchingModel, MatchingModel,
@ -15,7 +15,10 @@ import {
MATCH_NONE, MATCH_NONE,
} from 'src/app/data/matching-model' } from 'src/app/data/matching-model'
import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithId } from 'src/app/data/object-with-id'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' import {
ObjectWithPermissions,
PermissionsObject,
} from 'src/app/data/object-with-permissions'
import { import {
SortableDirective, SortableDirective,
SortEvent, SortEvent,
@ -28,11 +31,9 @@ import {
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service' import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
EditDialogComponent,
EditDialogMode,
} from '../../common/edit-dialog/edit-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
export interface ManagementListColumn { export interface ManagementListColumn {
key: string key: string
@ -82,6 +83,8 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
private unsubscribeNotifier: Subject<any> = new Subject() private unsubscribeNotifier: Subject<any> = new Subject()
private _nameFilter: string private _nameFilter: string
public selectedObjects: Set<number> = new Set()
ngOnInit(): void { ngOnInit(): void {
this.reloadData() this.reloadData()
@ -243,4 +246,63 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
object object
) )
} }
get userOwnsAll(): boolean {
let ownsAll: boolean = true
const objects = this.data.filter((o) => this.selectedObjects.has(o.id))
ownsAll = objects.every((o) =>
this.permissionsService.currentUserOwnsObject(o)
)
return ownsAll
}
toggleAll(event: PointerEvent) {
if ((event.target as HTMLInputElement).checked) {
this.selectedObjects = new Set(this.data.map((o) => o.id))
} else {
this.clearSelection()
}
}
clearSelection() {
this.selectedObjects.clear()
}
toggleSelected(object) {
this.selectedObjects.has(object.id)
? this.selectedObjects.delete(object.id)
: this.selectedObjects.add(object.id)
}
setPermissions() {
let modal = this.modalService.open(PermissionsDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.confirmClicked.subscribe(
(permissions: { owner: number; set_permissions: PermissionsObject }) => {
modal.componentInstance.buttonsEnabled = false
this.service
.bulk_update_permissions(
Array.from(this.selectedObjects),
permissions
)
.subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
$localize`Permissions updated successfully`
)
this.reloadData()
},
error: (error) => {
modal.componentInstance.buttonsEnabled = true
this.toastService.showError(
$localize`Error updating permissions`,
error
)
},
})
}
)
}
} }

View File

@ -47,12 +47,12 @@
<tbody> <tbody>
<ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize"> <ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize">
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();"> <tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<th> <td>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();"> <input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label> <label class="form-check-label" for="task{{task.id}}"></label>
</div> </div>
</th> </td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td> <td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td> <td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'"> <td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'">

View File

@ -1,5 +1,4 @@
import { ObjectWithId } from './object-with-id' import { ObjectWithId } from './object-with-id'
import { PaperlessUser } from './paperless-user'
export interface PermissionsObject { export interface PermissionsObject {
view: { view: {

View File

@ -39,6 +39,31 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
expect(req.request.method).toEqual('GET') expect(req.request.method).toEqual('GET')
req.flush([]) req.flush([])
}) })
test('should call appropriate api endpoint for bulk permissions edit', () => {
const owner = 3
const permissions = {
view: {
users: [],
groups: [3],
},
change: {
users: [12, 13],
groups: [],
},
}
subscription = service
.bulk_update_permissions([1, 2], {
owner,
set_permissions: permissions,
})
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}bulk_edit_object_perms/`
)
expect(req.request.method).toEqual('POST')
req.flush([])
})
}) })
beforeEach(() => { beforeEach(() => {

View File

@ -1,5 +1,7 @@
import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithId } from 'src/app/data/object-with-id'
import { AbstractPaperlessService } from './abstract-paperless-service' import { AbstractPaperlessService } from './abstract-paperless-service'
import { PermissionsObject } from 'src/app/data/object-with-permissions'
import { Observable } from 'rxjs'
export abstract class AbstractNameFilterService< export abstract class AbstractNameFilterService<
T extends ObjectWithId, T extends ObjectWithId,
@ -21,4 +23,16 @@ export abstract class AbstractNameFilterService<
} }
return this.list(page, pageSize, sortField, sortReverse, params) return this.list(page, pageSize, sortField, sortReverse, params)
} }
bulk_update_permissions(
objects: Array<number>,
permissions: { owner: number; set_permissions: PermissionsObject }
): Observable<string> {
return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
objects,
object_type: this.resourceName,
owner: permissions.owner,
permissions: permissions.set_permissions,
})
}
} }

View File

@ -973,10 +973,10 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
object_type = serializers.ChoiceField( object_type = serializers.ChoiceField(
choices=[ choices=[
"tag", "tags",
"correspondent", "correspondents",
"document_type", "document_types",
"storage_path", "storage_paths",
], ],
label="Object Type", label="Object Type",
write_only=True, write_only=True,
@ -997,13 +997,13 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
def get_object_class(self, object_type): def get_object_class(self, object_type):
object_class = None object_class = None
if object_type == "tag": if object_type == "tags":
object_class = Tag object_class = Tag
elif object_type == "correspondent": elif object_type == "correspondents":
object_class = Correspondent object_class = Correspondent
elif object_type == "document_type": elif object_type == "document_types":
object_class = DocumentType object_class = DocumentType
elif object_type == "storage_path": elif object_type == "storage_paths":
object_class = StoragePath object_class = StoragePath
return object_class return object_class
@ -1013,10 +1013,6 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
if not all(isinstance(i, int) for i in objects): if not all(isinstance(i, int) for i in objects):
raise serializers.ValidationError("objects must be a list of integers") raise serializers.ValidationError("objects must be a list of integers")
object_class = self.get_object_class(object_type) object_class = self.get_object_class(object_type)
if object_class is None:
raise serializers.ValidationError(
"Unknown object type.",
)
count = object_class.objects.filter(id__in=objects).count() count = object_class.objects.filter(id__in=objects).count()
if not count == len(objects): if not count == len(objects):
raise serializers.ValidationError( raise serializers.ValidationError(

View File

@ -5100,6 +5100,9 @@ class TestBulkEditObjectPermissions(APITestCase):
self.t1 = Tag.objects.create(name="t1") self.t1 = Tag.objects.create(name="t1")
self.t2 = Tag.objects.create(name="t2") self.t2 = Tag.objects.create(name="t2")
self.c1 = Correspondent.objects.create(name="c1")
self.dt1 = DocumentType.objects.create(name="dt1")
self.sp1 = StoragePath.objects.create(name="sp1")
self.user1 = User.objects.create(username="user1") self.user1 = User.objects.create(username="user1")
self.user2 = User.objects.create(username="user2") self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3") self.user3 = User.objects.create(username="user3")
@ -5129,7 +5132,7 @@ class TestBulkEditObjectPermissions(APITestCase):
json.dumps( json.dumps(
{ {
"objects": [self.t1.id, self.t2.id], "objects": [self.t1.id, self.t2.id],
"object_type": "tag", "object_type": "tags",
"permissions": permissions, "permissions": permissions,
}, },
), ),
@ -5189,7 +5192,7 @@ class TestBulkEditObjectPermissions(APITestCase):
json.dumps( json.dumps(
{ {
"objects": [self.t1.id, self.t2.id], "objects": [self.t1.id, self.t2.id],
"object_type": "tag", "object_type": "tags",
"owner": self.user3.id, "owner": self.user3.id,
}, },
), ),
@ -5232,7 +5235,7 @@ class TestBulkEditObjectPermissions(APITestCase):
json.dumps( json.dumps(
{ {
"objects": [self.t1.id, self.t2.id], "objects": [self.t1.id, self.t2.id],
"object_type": "tag", "object_type": "tags",
"owner": self.user1.id, "owner": self.user1.id,
}, },
), ),