Merge branch 'dev' into feature-confirm-buttons
This commit is contained in:
commit
ad773cd876
@ -375,14 +375,15 @@ The following methods are supported:
|
|||||||
|
|
||||||
### Objects
|
### Objects
|
||||||
|
|
||||||
Bulk editing for objects (tags, document types etc.) currently supports only updating permissions, using
|
Bulk editing for objects (tags, document types etc.) currently supports set permissions or delete
|
||||||
the endpoint: `/api/bulk_edit_object_perms/` which requires a json payload of the format:
|
operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json payload of the format:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"objects": [LIST_OF_OBJECT_IDS],
|
"objects": [LIST_OF_OBJECT_IDS],
|
||||||
"object_type": "tags", "correspondents", "document_types" or "storage_paths"
|
"object_type": "tags", "correspondents", "document_types" or "storage_paths",
|
||||||
"owner": OWNER_ID // optional
|
"operation": "set_permissions" or "delete",
|
||||||
|
"owner": OWNER_ID, // optional
|
||||||
"permissions": { "view": { "users": [] ... }, "change": { ... } }, // (see 'set_permissions' format above)
|
"permissions": { "view": { "users": [] ... }, "change": { ... } }, // (see 'set_permissions' format above)
|
||||||
"merge": true / false // defaults to false, see above
|
"merge": true / false // defaults to false, see above
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
||||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary me-5" (click)="setPermissions()" [disabled]="!userOwnsAll || selectedObjects.size === 0">
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || selectedObjects.size === 0">
|
||||||
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger me-5" (click)="delete()" [disabled]="!userOwnsAll || selectedObjects.size === 0">
|
||||||
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@ -39,6 +39,7 @@ import { MATCH_LITERAL } from 'src/app/data/matching-model'
|
|||||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||||
|
import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-filter-service'
|
||||||
|
|
||||||
const tags: Tag[] = [
|
const tags: Tag[] = [
|
||||||
{
|
{
|
||||||
@ -153,7 +154,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'))[2]
|
const createButton = fixture.debugElement.queryAll(By.css('button'))[3]
|
||||||
createButton.triggerEventHandler('click')
|
createButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@ -177,7 +178,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'))[6]
|
const editButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
||||||
editButton.triggerEventHandler('click')
|
editButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@ -218,7 +219,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'))[5]
|
const filterButton = fixture.debugElement.queryAll(By.css('button'))[6]
|
||||||
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() },
|
||||||
@ -246,7 +247,7 @@ describe('ManagementListComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support bulk edit permissions', () => {
|
it('should support bulk edit permissions', () => {
|
||||||
const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_update_permissions')
|
const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_edit_objects')
|
||||||
component.toggleSelected(tags[0])
|
component.toggleSelected(tags[0])
|
||||||
component.toggleSelected(tags[1])
|
component.toggleSelected(tags[1])
|
||||||
component.toggleSelected(tags[2])
|
component.toggleSelected(tags[2])
|
||||||
@ -280,4 +281,35 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(bulkEditPermsSpy).toHaveBeenCalled()
|
expect(bulkEditPermsSpy).toHaveBeenCalled()
|
||||||
expect(successToastSpy).toHaveBeenCalled()
|
expect(successToastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support bulk delete objects', () => {
|
||||||
|
const bulkEditSpy = jest.spyOn(tagService, 'bulk_edit_objects')
|
||||||
|
component.toggleSelected(tags[0])
|
||||||
|
component.toggleSelected(tags[1])
|
||||||
|
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.delete()
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
|
||||||
|
// fail first
|
||||||
|
bulkEditSpy.mockReturnValueOnce(
|
||||||
|
throwError(() => new Error('error setting permissions'))
|
||||||
|
)
|
||||||
|
const errorToastSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
modal.componentInstance.confirmClicked.emit(null)
|
||||||
|
expect(bulkEditSpy).toHaveBeenCalledWith(
|
||||||
|
Array.from(selected),
|
||||||
|
BulkEditObjectOperation.Delete
|
||||||
|
)
|
||||||
|
expect(errorToastSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
const successToastSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
|
bulkEditSpy.mockReturnValueOnce(of('OK'))
|
||||||
|
modal.componentInstance.confirmClicked.emit(null)
|
||||||
|
expect(bulkEditSpy).toHaveBeenCalled()
|
||||||
|
expect(successToastSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -25,7 +25,10 @@ import {
|
|||||||
PermissionsService,
|
PermissionsService,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
|
import {
|
||||||
|
AbstractNameFilterService,
|
||||||
|
BulkEditObjectOperation,
|
||||||
|
} 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 { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
@ -266,8 +269,9 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
({ permissions, merge }) => {
|
({ permissions, merge }) => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.service
|
this.service
|
||||||
.bulk_update_permissions(
|
.bulk_edit_objects(
|
||||||
Array.from(this.selectedObjects),
|
Array.from(this.selectedObjects),
|
||||||
|
BulkEditObjectOperation.SetPermissions,
|
||||||
permissions,
|
permissions,
|
||||||
merge
|
merge
|
||||||
)
|
)
|
||||||
@ -290,4 +294,37 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.title = $localize`Confirm delete`
|
||||||
|
modal.componentInstance.messageBold = $localize`This operation will permanently delete all objects.`
|
||||||
|
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||||
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
|
this.service
|
||||||
|
.bulk_edit_objects(
|
||||||
|
Array.from(this.selectedObjects),
|
||||||
|
BulkEditObjectOperation.Delete
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
modal.close()
|
||||||
|
this.toastService.showInfo($localize`Objects deleted successfully`)
|
||||||
|
this.reloadData()
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
modal.componentInstance.buttonsEnabled = true
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error deleting objects`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,10 @@ import { HttpTestingController } from '@angular/common/http/testing'
|
|||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { TestBed } from '@angular/core/testing'
|
import { TestBed } from '@angular/core/testing'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { AbstractNameFilterService } from './abstract-name-filter-service'
|
import {
|
||||||
|
AbstractNameFilterService,
|
||||||
|
BulkEditObjectOperation,
|
||||||
|
} from './abstract-name-filter-service'
|
||||||
import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
|
import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
|
||||||
|
|
||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
@ -53,8 +56,9 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
subscription = service
|
subscription = service
|
||||||
.bulk_update_permissions(
|
.bulk_edit_objects(
|
||||||
[1, 2],
|
[1, 2],
|
||||||
|
BulkEditObjectOperation.SetPermissions,
|
||||||
{
|
{
|
||||||
owner,
|
owner,
|
||||||
set_permissions: permissions,
|
set_permissions: permissions,
|
||||||
@ -63,9 +67,33 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
|
|||||||
)
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}bulk_edit_object_perms/`
|
`${environment.apiBaseUrl}bulk_edit_objects/`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('POST')
|
expect(req.request.method).toEqual('POST')
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
objects: [1, 2],
|
||||||
|
object_type: endpoint,
|
||||||
|
operation: BulkEditObjectOperation.SetPermissions,
|
||||||
|
permissions,
|
||||||
|
owner,
|
||||||
|
merge: true,
|
||||||
|
})
|
||||||
|
req.flush([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should call appropriate api endpoint for bulk delete objects', () => {
|
||||||
|
subscription = service
|
||||||
|
.bulk_edit_objects([1, 2], BulkEditObjectOperation.Delete)
|
||||||
|
.subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}bulk_edit_objects/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('POST')
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
objects: [1, 2],
|
||||||
|
object_type: endpoint,
|
||||||
|
operation: BulkEditObjectOperation.Delete,
|
||||||
|
})
|
||||||
req.flush([])
|
req.flush([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,6 +3,11 @@ import { AbstractPaperlessService } from './abstract-paperless-service'
|
|||||||
import { PermissionsObject } from 'src/app/data/object-with-permissions'
|
import { PermissionsObject } from 'src/app/data/object-with-permissions'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
|
export enum BulkEditObjectOperation {
|
||||||
|
SetPermissions = 'set_permissions',
|
||||||
|
Delete = 'delete',
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class AbstractNameFilterService<
|
export abstract class AbstractNameFilterService<
|
||||||
T extends ObjectWithId,
|
T extends ObjectWithId,
|
||||||
> extends AbstractPaperlessService<T> {
|
> extends AbstractPaperlessService<T> {
|
||||||
@ -24,17 +29,22 @@ export abstract class AbstractNameFilterService<
|
|||||||
return this.list(page, pageSize, sortField, sortReverse, params)
|
return this.list(page, pageSize, sortField, sortReverse, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
bulk_update_permissions(
|
bulk_edit_objects(
|
||||||
objects: Array<number>,
|
objects: Array<number>,
|
||||||
permissions: { owner: number; set_permissions: PermissionsObject },
|
operation: BulkEditObjectOperation,
|
||||||
merge: boolean
|
permissions: { owner: number; set_permissions: PermissionsObject } = null,
|
||||||
|
merge: boolean = null
|
||||||
): Observable<string> {
|
): Observable<string> {
|
||||||
return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
|
const params = {
|
||||||
objects,
|
objects,
|
||||||
object_type: this.resourceName,
|
object_type: this.resourceName,
|
||||||
owner: permissions.owner,
|
operation,
|
||||||
permissions: permissions.set_permissions,
|
}
|
||||||
merge,
|
if (operation === BulkEditObjectOperation.SetPermissions) {
|
||||||
})
|
params['owner'] = permissions?.owner
|
||||||
|
params['permissions'] = permissions?.set_permissions
|
||||||
|
params['merge'] = merge
|
||||||
|
}
|
||||||
|
return this.http.post<string>(`${this.baseUrl}bulk_edit_objects/`, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI)
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
apiBaseUrl: document.baseURI + 'api/',
|
apiBaseUrl: document.baseURI + 'api/',
|
||||||
apiVersion: '4',
|
apiVersion: '5',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
version: '2.4.3-dev',
|
version: '2.4.3-dev',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBaseUrl: 'http://localhost:8000/api/',
|
apiBaseUrl: 'http://localhost:8000/api/',
|
||||||
apiVersion: '4',
|
apiVersion: '5',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
version: 'DEVELOPMENT',
|
version: 'DEVELOPMENT',
|
||||||
webSocketHost: 'localhost:8000',
|
webSocketHost: 'localhost:8000',
|
||||||
|
@ -1281,7 +1281,7 @@ class ShareLinkSerializer(OwnedObjectSerializer):
|
|||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissionsMixin):
|
class BulkEditObjectsSerializer(serializers.Serializer, SetPermissionsMixin):
|
||||||
objects = serializers.ListField(
|
objects = serializers.ListField(
|
||||||
required=True,
|
required=True,
|
||||||
allow_empty=False,
|
allow_empty=False,
|
||||||
@ -1301,6 +1301,16 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
operation = serializers.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
"set_permissions",
|
||||||
|
"delete",
|
||||||
|
],
|
||||||
|
label="Operation",
|
||||||
|
required=True,
|
||||||
|
write_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
owner = serializers.PrimaryKeyRelatedField(
|
owner = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1353,11 +1363,14 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
|||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
object_type = attrs["object_type"]
|
object_type = attrs["object_type"]
|
||||||
objects = attrs["objects"]
|
objects = attrs["objects"]
|
||||||
permissions = attrs.get("permissions")
|
operation = attrs.get("operation")
|
||||||
|
|
||||||
self._validate_objects(objects, object_type)
|
self._validate_objects(objects, object_type)
|
||||||
if permissions is not None:
|
|
||||||
self._validate_permissions(permissions)
|
if operation == "set_permissions":
|
||||||
|
permissions = attrs.get("permissions")
|
||||||
|
if permissions is not None:
|
||||||
|
self._validate_permissions(permissions)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -222,3 +222,118 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
|||||||
args, _ = bulk_update_mock.call_args
|
args, _ = bulk_update_mock.call_args
|
||||||
|
|
||||||
self.assertCountEqual([document.pk], args[0])
|
self.assertCountEqual([document.pk], args[0])
|
||||||
|
|
||||||
|
|
||||||
|
class TestBulkEditObjects(APITestCase):
|
||||||
|
# See test_api_permissions.py for bulk tests on permissions
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.temp_admin = User.objects.create_superuser(username="temp_admin")
|
||||||
|
self.client.force_authenticate(user=self.temp_admin)
|
||||||
|
|
||||||
|
self.t1 = Tag.objects.create(name="t1")
|
||||||
|
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.user2 = User.objects.create(username="user2")
|
||||||
|
self.user3 = User.objects.create(username="user3")
|
||||||
|
|
||||||
|
def test_bulk_objects_delete(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with delete operation
|
||||||
|
THEN:
|
||||||
|
- Objects are deleted
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id, self.t2.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(Tag.objects.count(), 0)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.c1.id],
|
||||||
|
"object_type": "correspondents",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(Correspondent.objects.count(), 0)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.dt1.id],
|
||||||
|
"object_type": "document_types",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(DocumentType.objects.count(), 0)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.sp1.id],
|
||||||
|
"object_type": "storage_paths",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(StoragePath.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_bulk_edit_object_permissions_insufficient_perms(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Objects owned by user other than logged in user
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with delete operation
|
||||||
|
THEN:
|
||||||
|
- User is not able to delete objects
|
||||||
|
"""
|
||||||
|
self.t1.owner = User.objects.get(username="temp_admin")
|
||||||
|
self.t1.save()
|
||||||
|
self.client.force_authenticate(user=self.user1)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id, self.t2.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "delete",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(response.content, b"Insufficient permissions")
|
||||||
|
@ -717,7 +717,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing objects
|
- Existing objects
|
||||||
WHEN:
|
WHEN:
|
||||||
- bulk_edit_object_perms API endpoint is called
|
- bulk_edit_objects API endpoint is called with set_permissions operation
|
||||||
THEN:
|
THEN:
|
||||||
- Permissions and / or owner are changed
|
- Permissions and / or owner are changed
|
||||||
"""
|
"""
|
||||||
@ -733,11 +733,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.t1.id, self.t2.id],
|
"objects": [self.t1.id, self.t2.id],
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -748,11 +749,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertIn(self.user1, get_users_with_perms(self.t1))
|
self.assertIn(self.user1, get_users_with_perms(self.t1))
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.c1.id],
|
"objects": [self.c1.id],
|
||||||
"object_type": "correspondents",
|
"object_type": "correspondents",
|
||||||
|
"operation": "set_permissions",
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -763,11 +765,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertIn(self.user1, get_users_with_perms(self.c1))
|
self.assertIn(self.user1, get_users_with_perms(self.c1))
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.dt1.id],
|
"objects": [self.dt1.id],
|
||||||
"object_type": "document_types",
|
"object_type": "document_types",
|
||||||
|
"operation": "set_permissions",
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -778,11 +781,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertIn(self.user1, get_users_with_perms(self.dt1))
|
self.assertIn(self.user1, get_users_with_perms(self.dt1))
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.sp1.id],
|
"objects": [self.sp1.id],
|
||||||
"object_type": "storage_paths",
|
"object_type": "storage_paths",
|
||||||
|
"operation": "set_permissions",
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -793,11 +797,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertIn(self.user1, get_users_with_perms(self.sp1))
|
self.assertIn(self.user1, get_users_with_perms(self.sp1))
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.t1.id, self.t2.id],
|
"objects": [self.t1.id, self.t2.id],
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
"owner": self.user3.id,
|
"owner": self.user3.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -808,11 +813,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
|
self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.sp1.id],
|
"objects": [self.sp1.id],
|
||||||
"object_type": "storage_paths",
|
"object_type": "storage_paths",
|
||||||
|
"operation": "set_permissions",
|
||||||
"owner": self.user3.id,
|
"owner": self.user3.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -827,7 +833,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing objects
|
- Existing objects
|
||||||
WHEN:
|
WHEN:
|
||||||
- bulk_edit_object_perms API endpoint is called with merge=True or merge=False (default)
|
- bulk_edit_objects API endpoint is called with set_permissions operation with merge=True or merge=False (default)
|
||||||
THEN:
|
THEN:
|
||||||
- Permissions and / or owner are replaced or merged, depending on the merge flag
|
- Permissions and / or owner are replaced or merged, depending on the merge flag
|
||||||
"""
|
"""
|
||||||
@ -848,13 +854,14 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
|
|
||||||
# merge=True
|
# merge=True
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.t1.id, self.t2.id],
|
"objects": [self.t1.id, self.t2.id],
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
"owner": self.user1.id,
|
"owner": self.user1.id,
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
|
"operation": "set_permissions",
|
||||||
"merge": True,
|
"merge": True,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -877,12 +884,13 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
|
|
||||||
# merge=False (default)
|
# merge=False (default)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.t1.id, self.t2.id],
|
"objects": [self.t1.id, self.t2.id],
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
|
"operation": "set_permissions",
|
||||||
"merge": False,
|
"merge": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -900,7 +908,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
GIVEN:
|
GIVEN:
|
||||||
- Objects owned by user other than logged in user
|
- Objects owned by user other than logged in user
|
||||||
WHEN:
|
WHEN:
|
||||||
- bulk_edit_object_perms API endpoint is called
|
- bulk_edit_objects API endpoint is called with set_permissions operation
|
||||||
THEN:
|
THEN:
|
||||||
- User is not able to change permissions
|
- User is not able to change permissions
|
||||||
"""
|
"""
|
||||||
@ -909,11 +917,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.client.force_authenticate(user=self.user1)
|
self.client.force_authenticate(user=self.user1)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.t1.id, self.t2.id],
|
"objects": [self.t1.id, self.t2.id],
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
"owner": self.user1.id,
|
"owner": self.user1.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -928,17 +937,18 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing objects
|
- Existing objects
|
||||||
WHEN:
|
WHEN:
|
||||||
- bulk_edit_object_perms API endpoint is called with invalid params
|
- bulk_edit_objects API endpoint is called with set_permissions operation with invalid params
|
||||||
THEN:
|
THEN:
|
||||||
- Validation fails
|
- Validation fails
|
||||||
"""
|
"""
|
||||||
# not a list
|
# not a list
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": self.t1.id,
|
"objects": self.t1.id,
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
"owner": self.user1.id,
|
"owner": self.user1.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -949,7 +959,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
|
|
||||||
# not a list of ints
|
# not a list of ints
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": ["one"],
|
"objects": ["one"],
|
||||||
@ -964,11 +974,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
|
|
||||||
# duplicates
|
# duplicates
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [self.t1.id, self.t2.id, self.t1.id],
|
"objects": [self.t1.id, self.t2.id, self.t1.id],
|
||||||
"object_type": "tags",
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
"owner": self.user1.id,
|
"owner": self.user1.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -979,11 +990,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
|
|
||||||
# not a valid object type
|
# not a valid object type
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/bulk_edit_object_perms/",
|
"/api/bulk_edit_objects/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"objects": [1],
|
"objects": [1],
|
||||||
"object_type": "madeup",
|
"object_type": "madeup",
|
||||||
|
"operation": "set_permissions",
|
||||||
"owner": self.user1.id,
|
"owner": self.user1.id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -115,7 +115,7 @@ from documents.permissions import has_perms_owner_aware
|
|||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
from documents.serialisers import AcknowledgeTasksViewSerializer
|
from documents.serialisers import AcknowledgeTasksViewSerializer
|
||||||
from documents.serialisers import BulkDownloadSerializer
|
from documents.serialisers import BulkDownloadSerializer
|
||||||
from documents.serialisers import BulkEditObjectPermissionsSerializer
|
from documents.serialisers import BulkEditObjectsSerializer
|
||||||
from documents.serialisers import BulkEditSerializer
|
from documents.serialisers import BulkEditSerializer
|
||||||
from documents.serialisers import CorrespondentSerializer
|
from documents.serialisers import CorrespondentSerializer
|
||||||
from documents.serialisers import CustomFieldSerializer
|
from documents.serialisers import CustomFieldSerializer
|
||||||
@ -1401,9 +1401,9 @@ def serve_file(doc: Document, use_archive: bool, disposition: str):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
class BulkEditObjectsView(GenericAPIView, PassUserMixin):
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
serializer_class = BulkEditObjectPermissionsSerializer
|
serializer_class = BulkEditObjectsSerializer
|
||||||
parser_classes = (parsers.JSONParser,)
|
parser_classes = (parsers.JSONParser,)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -1414,42 +1414,52 @@ class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
|||||||
object_type = serializer.validated_data.get("object_type")
|
object_type = serializer.validated_data.get("object_type")
|
||||||
object_ids = serializer.validated_data.get("objects")
|
object_ids = serializer.validated_data.get("objects")
|
||||||
object_class = serializer.get_object_class(object_type)
|
object_class = serializer.get_object_class(object_type)
|
||||||
permissions = serializer.validated_data.get("permissions")
|
operation = serializer.validated_data.get("operation")
|
||||||
owner = serializer.validated_data.get("owner")
|
|
||||||
merge = serializer.validated_data.get("merge")
|
objs = object_class.objects.filter(pk__in=object_ids)
|
||||||
|
|
||||||
if not user.is_superuser:
|
if not user.is_superuser:
|
||||||
objs = object_class.objects.filter(pk__in=object_ids)
|
|
||||||
has_perms = all((obj.owner == user or obj.owner is None) for obj in objs)
|
has_perms = all((obj.owner == user or obj.owner is None) for obj in objs)
|
||||||
|
|
||||||
if not has_perms:
|
if not has_perms:
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
try:
|
if operation == "set_permissions":
|
||||||
qs = object_class.objects.filter(id__in=object_ids)
|
permissions = serializer.validated_data.get("permissions")
|
||||||
|
owner = serializer.validated_data.get("owner")
|
||||||
|
merge = serializer.validated_data.get("merge")
|
||||||
|
|
||||||
# if merge is true, we dont want to remove the owner
|
try:
|
||||||
if "owner" in serializer.validated_data and (
|
qs = object_class.objects.filter(id__in=object_ids)
|
||||||
not merge or (merge and owner is not None)
|
|
||||||
):
|
|
||||||
# if merge is true, we dont want to overwrite the owner
|
|
||||||
qs_owner_update = qs.filter(owner__isnull=True) if merge else qs
|
|
||||||
qs_owner_update.update(owner=owner)
|
|
||||||
|
|
||||||
if "permissions" in serializer.validated_data:
|
# if merge is true, we dont want to remove the owner
|
||||||
for obj in qs:
|
if "owner" in serializer.validated_data and (
|
||||||
set_permissions_for_object(
|
not merge or (merge and owner is not None)
|
||||||
permissions=permissions,
|
):
|
||||||
object=obj,
|
# if merge is true, we dont want to overwrite the owner
|
||||||
merge=merge,
|
qs_owner_update = qs.filter(owner__isnull=True) if merge else qs
|
||||||
)
|
qs_owner_update.update(owner=owner)
|
||||||
|
|
||||||
return Response({"result": "OK"})
|
if "permissions" in serializer.validated_data:
|
||||||
except Exception as e:
|
for obj in qs:
|
||||||
logger.warning(f"An error occurred performing bulk permissions edit: {e!s}")
|
set_permissions_for_object(
|
||||||
return HttpResponseBadRequest(
|
permissions=permissions,
|
||||||
"Error performing bulk permissions edit, check logs for more detail.",
|
object=obj,
|
||||||
)
|
merge=merge,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"An error occurred performing bulk permissions edit: {e!s}",
|
||||||
|
)
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Error performing bulk permissions edit, check logs for more detail.",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif operation == "delete":
|
||||||
|
objs.delete()
|
||||||
|
|
||||||
|
return Response({"result": "OK"})
|
||||||
|
|
||||||
|
|
||||||
class WorkflowTriggerViewSet(ModelViewSet):
|
class WorkflowTriggerViewSet(ModelViewSet):
|
||||||
|
@ -322,7 +322,7 @@ REST_FRAMEWORK = {
|
|||||||
"DEFAULT_VERSION": "1",
|
"DEFAULT_VERSION": "1",
|
||||||
# Make sure these are ordered and that the most recent version appears
|
# Make sure these are ordered and that the most recent version appears
|
||||||
# last
|
# last
|
||||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4"],
|
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5"],
|
||||||
}
|
}
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
@ -16,7 +16,7 @@ from rest_framework.routers import DefaultRouter
|
|||||||
|
|
||||||
from documents.views import AcknowledgeTasksView
|
from documents.views import AcknowledgeTasksView
|
||||||
from documents.views import BulkDownloadView
|
from documents.views import BulkDownloadView
|
||||||
from documents.views import BulkEditObjectPermissionsView
|
from documents.views import BulkEditObjectsView
|
||||||
from documents.views import BulkEditView
|
from documents.views import BulkEditView
|
||||||
from documents.views import CorrespondentViewSet
|
from documents.views import CorrespondentViewSet
|
||||||
from documents.views import CustomFieldViewSet
|
from documents.views import CustomFieldViewSet
|
||||||
@ -129,9 +129,9 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("token/", views.obtain_auth_token),
|
path("token/", views.obtain_auth_token),
|
||||||
re_path(
|
re_path(
|
||||||
"^bulk_edit_object_perms/",
|
"^bulk_edit_objects/",
|
||||||
BulkEditObjectPermissionsView.as_view(),
|
BulkEditObjectsView.as_view(),
|
||||||
name="bulk_edit_object_permissions",
|
name="bulk_edit_objects",
|
||||||
),
|
),
|
||||||
path("profile/generate_auth_token/", GenerateAuthTokenView.as_view()),
|
path("profile/generate_auth_token/", GenerateAuthTokenView.as_view()),
|
||||||
path(
|
path(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user