Merge branch 'dev' into feature-confirm-buttons
This commit is contained in:
commit
ad773cd876
@ -375,14 +375,15 @@ The following methods are supported:
|
||||
|
||||
### Objects
|
||||
|
||||
Bulk editing for objects (tags, document types etc.) currently supports only updating permissions, using
|
||||
the endpoint: `/api/bulk_edit_object_perms/` which requires a json payload of the format:
|
||||
Bulk editing for objects (tags, document types etc.) currently supports set permissions or delete
|
||||
operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json payload of the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [LIST_OF_OBJECT_IDS],
|
||||
"object_type": "tags", "correspondents", "document_types" or "storage_paths"
|
||||
"owner": OWNER_ID // optional
|
||||
"object_type": "tags", "correspondents", "document_types" or "storage_paths",
|
||||
"operation": "set_permissions" or "delete",
|
||||
"owner": OWNER_ID, // optional
|
||||
"permissions": { "view": { "users": [] ... }, "change": { ... } }, // (see 'set_permissions' format 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">
|
||||
<i-bs name="x"></i-bs> <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">
|
||||
<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>
|
||||
</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 }">
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
||||
</button>
|
||||
|
@ -39,6 +39,7 @@ import { MATCH_LITERAL } from 'src/app/data/matching-model'
|
||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||
import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-filter-service'
|
||||
|
||||
const tags: Tag[] = [
|
||||
{
|
||||
@ -153,7 +154,7 @@ describe('ManagementListComponent', () => {
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
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')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
@ -177,7 +178,7 @@ describe('ManagementListComponent', () => {
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
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')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
@ -218,7 +219,7 @@ describe('ManagementListComponent', () => {
|
||||
|
||||
it('should support quick filter for objects', () => {
|
||||
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')
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
|
||||
@ -246,7 +247,7 @@ describe('ManagementListComponent', () => {
|
||||
})
|
||||
|
||||
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[1])
|
||||
component.toggleSelected(tags[2])
|
||||
@ -280,4 +281,35 @@ describe('ManagementListComponent', () => {
|
||||
expect(bulkEditPermsSpy).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,
|
||||
PermissionType,
|
||||
} 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 { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
@ -266,8 +269,9 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
({ permissions, merge }) => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.service
|
||||
.bulk_update_permissions(
|
||||
.bulk_edit_objects(
|
||||
Array.from(this.selectedObjects),
|
||||
BulkEditObjectOperation.SetPermissions,
|
||||
permissions,
|
||||
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 { TestBed } from '@angular/core/testing'
|
||||
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'
|
||||
|
||||
let httpTestingController: HttpTestingController
|
||||
@ -53,8 +56,9 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
|
||||
},
|
||||
}
|
||||
subscription = service
|
||||
.bulk_update_permissions(
|
||||
.bulk_edit_objects(
|
||||
[1, 2],
|
||||
BulkEditObjectOperation.SetPermissions,
|
||||
{
|
||||
owner,
|
||||
set_permissions: permissions,
|
||||
@ -63,9 +67,33 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
|
||||
)
|
||||
.subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}bulk_edit_object_perms/`
|
||||
`${environment.apiBaseUrl}bulk_edit_objects/`
|
||||
)
|
||||
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([])
|
||||
})
|
||||
})
|
||||
|
@ -3,6 +3,11 @@ import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||
import { PermissionsObject } from 'src/app/data/object-with-permissions'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
export enum BulkEditObjectOperation {
|
||||
SetPermissions = 'set_permissions',
|
||||
Delete = 'delete',
|
||||
}
|
||||
|
||||
export abstract class AbstractNameFilterService<
|
||||
T extends ObjectWithId,
|
||||
> extends AbstractPaperlessService<T> {
|
||||
@ -24,17 +29,22 @@ export abstract class AbstractNameFilterService<
|
||||
return this.list(page, pageSize, sortField, sortReverse, params)
|
||||
}
|
||||
|
||||
bulk_update_permissions(
|
||||
bulk_edit_objects(
|
||||
objects: Array<number>,
|
||||
permissions: { owner: number; set_permissions: PermissionsObject },
|
||||
merge: boolean
|
||||
operation: BulkEditObjectOperation,
|
||||
permissions: { owner: number; set_permissions: PermissionsObject } = null,
|
||||
merge: boolean = null
|
||||
): Observable<string> {
|
||||
return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
|
||||
const params = {
|
||||
objects,
|
||||
object_type: this.resourceName,
|
||||
owner: permissions.owner,
|
||||
permissions: permissions.set_permissions,
|
||||
merge,
|
||||
})
|
||||
operation,
|
||||
}
|
||||
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 = {
|
||||
production: true,
|
||||
apiBaseUrl: document.baseURI + 'api/',
|
||||
apiVersion: '4',
|
||||
apiVersion: '5',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: '2.4.3-dev',
|
||||
webSocketHost: window.location.host,
|
||||
|
@ -5,7 +5,7 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:8000/api/',
|
||||
apiVersion: '4',
|
||||
apiVersion: '5',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: 'DEVELOPMENT',
|
||||
webSocketHost: 'localhost:8000',
|
||||
|
@ -1281,7 +1281,7 @@ class ShareLinkSerializer(OwnedObjectSerializer):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissionsMixin):
|
||||
class BulkEditObjectsSerializer(serializers.Serializer, SetPermissionsMixin):
|
||||
objects = serializers.ListField(
|
||||
required=True,
|
||||
allow_empty=False,
|
||||
@ -1301,6 +1301,16 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
operation = serializers.ChoiceField(
|
||||
choices=[
|
||||
"set_permissions",
|
||||
"delete",
|
||||
],
|
||||
label="Operation",
|
||||
required=True,
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
owner = serializers.PrimaryKeyRelatedField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
@ -1353,11 +1363,14 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
||||
def validate(self, attrs):
|
||||
object_type = attrs["object_type"]
|
||||
objects = attrs["objects"]
|
||||
permissions = attrs.get("permissions")
|
||||
operation = attrs.get("operation")
|
||||
|
||||
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
|
||||
|
||||
|
@ -222,3 +222,118 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
||||
args, _ = bulk_update_mock.call_args
|
||||
|
||||
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:
|
||||
- Existing objects
|
||||
WHEN:
|
||||
- bulk_edit_object_perms API endpoint is called
|
||||
- bulk_edit_objects API endpoint is called with set_permissions operation
|
||||
THEN:
|
||||
- Permissions and / or owner are changed
|
||||
"""
|
||||
@ -733,11 +733,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"operation": "set_permissions",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
@ -748,11 +749,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.assertIn(self.user1, get_users_with_perms(self.t1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.c1.id],
|
||||
"object_type": "correspondents",
|
||||
"operation": "set_permissions",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
@ -763,11 +765,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.assertIn(self.user1, get_users_with_perms(self.c1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.dt1.id],
|
||||
"object_type": "document_types",
|
||||
"operation": "set_permissions",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
@ -778,11 +781,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.assertIn(self.user1, get_users_with_perms(self.dt1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.sp1.id],
|
||||
"object_type": "storage_paths",
|
||||
"operation": "set_permissions",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
@ -793,11 +797,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.assertIn(self.user1, get_users_with_perms(self.sp1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"operation": "set_permissions",
|
||||
"owner": self.user3.id,
|
||||
},
|
||||
),
|
||||
@ -808,11 +813,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.sp1.id],
|
||||
"object_type": "storage_paths",
|
||||
"operation": "set_permissions",
|
||||
"owner": self.user3.id,
|
||||
},
|
||||
),
|
||||
@ -827,7 +833,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
GIVEN:
|
||||
- Existing objects
|
||||
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:
|
||||
- Permissions and / or owner are replaced or merged, depending on the merge flag
|
||||
"""
|
||||
@ -848,13 +854,14 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
|
||||
# merge=True
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"owner": self.user1.id,
|
||||
"permissions": permissions,
|
||||
"operation": "set_permissions",
|
||||
"merge": True,
|
||||
},
|
||||
),
|
||||
@ -877,12 +884,13 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
|
||||
# merge=False (default)
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"permissions": permissions,
|
||||
"operation": "set_permissions",
|
||||
"merge": False,
|
||||
},
|
||||
),
|
||||
@ -900,7 +908,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
GIVEN:
|
||||
- Objects owned by user other than logged in user
|
||||
WHEN:
|
||||
- bulk_edit_object_perms API endpoint is called
|
||||
- bulk_edit_objects API endpoint is called with set_permissions operation
|
||||
THEN:
|
||||
- User is not able to change permissions
|
||||
"""
|
||||
@ -909,11 +917,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"operation": "set_permissions",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
@ -928,17 +937,18 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
GIVEN:
|
||||
- Existing objects
|
||||
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:
|
||||
- Validation fails
|
||||
"""
|
||||
# not a list
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": self.t1.id,
|
||||
"object_type": "tags",
|
||||
"operation": "set_permissions",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
@ -949,7 +959,7 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
|
||||
# not a list of ints
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": ["one"],
|
||||
@ -964,11 +974,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
|
||||
# duplicates
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id, self.t1.id],
|
||||
"object_type": "tags",
|
||||
"operation": "set_permissions",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
@ -979,11 +990,12 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
|
||||
# not a valid object type
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
"/api/bulk_edit_objects/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [1],
|
||||
"object_type": "madeup",
|
||||
"operation": "set_permissions",
|
||||
"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.serialisers import AcknowledgeTasksViewSerializer
|
||||
from documents.serialisers import BulkDownloadSerializer
|
||||
from documents.serialisers import BulkEditObjectPermissionsSerializer
|
||||
from documents.serialisers import BulkEditObjectsSerializer
|
||||
from documents.serialisers import BulkEditSerializer
|
||||
from documents.serialisers import CorrespondentSerializer
|
||||
from documents.serialisers import CustomFieldSerializer
|
||||
@ -1401,9 +1401,9 @@ def serve_file(doc: Document, use_archive: bool, disposition: str):
|
||||
return response
|
||||
|
||||
|
||||
class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
||||
class BulkEditObjectsView(GenericAPIView, PassUserMixin):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = BulkEditObjectPermissionsSerializer
|
||||
serializer_class = BulkEditObjectsSerializer
|
||||
parser_classes = (parsers.JSONParser,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -1414,42 +1414,52 @@ class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
||||
object_type = serializer.validated_data.get("object_type")
|
||||
object_ids = serializer.validated_data.get("objects")
|
||||
object_class = serializer.get_object_class(object_type)
|
||||
permissions = serializer.validated_data.get("permissions")
|
||||
owner = serializer.validated_data.get("owner")
|
||||
merge = serializer.validated_data.get("merge")
|
||||
operation = serializer.validated_data.get("operation")
|
||||
|
||||
objs = object_class.objects.filter(pk__in=object_ids)
|
||||
|
||||
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)
|
||||
|
||||
if not has_perms:
|
||||
return HttpResponseForbidden("Insufficient permissions")
|
||||
|
||||
try:
|
||||
qs = object_class.objects.filter(id__in=object_ids)
|
||||
if operation == "set_permissions":
|
||||
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
|
||||
if "owner" in serializer.validated_data and (
|
||||
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)
|
||||
try:
|
||||
qs = object_class.objects.filter(id__in=object_ids)
|
||||
|
||||
if "permissions" in serializer.validated_data:
|
||||
for obj in qs:
|
||||
set_permissions_for_object(
|
||||
permissions=permissions,
|
||||
object=obj,
|
||||
merge=merge,
|
||||
)
|
||||
# if merge is true, we dont want to remove the owner
|
||||
if "owner" in serializer.validated_data and (
|
||||
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)
|
||||
|
||||
return Response({"result": "OK"})
|
||||
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.",
|
||||
)
|
||||
if "permissions" in serializer.validated_data:
|
||||
for obj in qs:
|
||||
set_permissions_for_object(
|
||||
permissions=permissions,
|
||||
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):
|
||||
|
@ -322,7 +322,7 @@ REST_FRAMEWORK = {
|
||||
"DEFAULT_VERSION": "1",
|
||||
# Make sure these are ordered and that the most recent version appears
|
||||
# last
|
||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4"],
|
||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5"],
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
|
@ -16,7 +16,7 @@ from rest_framework.routers import DefaultRouter
|
||||
|
||||
from documents.views import AcknowledgeTasksView
|
||||
from documents.views import BulkDownloadView
|
||||
from documents.views import BulkEditObjectPermissionsView
|
||||
from documents.views import BulkEditObjectsView
|
||||
from documents.views import BulkEditView
|
||||
from documents.views import CorrespondentViewSet
|
||||
from documents.views import CustomFieldViewSet
|
||||
@ -129,9 +129,9 @@ urlpatterns = [
|
||||
),
|
||||
path("token/", views.obtain_auth_token),
|
||||
re_path(
|
||||
"^bulk_edit_object_perms/",
|
||||
BulkEditObjectPermissionsView.as_view(),
|
||||
name="bulk_edit_object_permissions",
|
||||
"^bulk_edit_objects/",
|
||||
BulkEditObjectsView.as_view(),
|
||||
name="bulk_edit_objects",
|
||||
),
|
||||
path("profile/generate_auth_token/", GenerateAuthTokenView.as_view()),
|
||||
path(
|
||||
|
Loading…
x
Reference in New Issue
Block a user