diff --git a/docs/api.md b/docs/api.md index bd5154ada..fa32bf2aa 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html index 9a6a6cc35..d627a1540 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.html +++ b/src-ui/src/app/components/manage/management-list/management-list.component.html @@ -2,9 +2,12 @@ - + diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts index 20f1c3a1d..280c40ca8 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts @@ -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() + }) }) diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index 101436883..8f0947f1c 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -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 ({ 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 } ) } + + 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 + ) + }, + }) + }) + } } diff --git a/src-ui/src/app/services/rest/abstract-name-filter-service.spec.ts b/src-ui/src/app/services/rest/abstract-name-filter-service.spec.ts index e09270701..f61efc640 100644 --- a/src-ui/src/app/services/rest/abstract-name-filter-service.spec.ts +++ b/src-ui/src/app/services/rest/abstract-name-filter-service.spec.ts @@ -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([]) }) }) diff --git a/src-ui/src/app/services/rest/abstract-name-filter-service.ts b/src-ui/src/app/services/rest/abstract-name-filter-service.ts index b38994086..1018f0fa2 100644 --- a/src-ui/src/app/services/rest/abstract-name-filter-service.ts +++ b/src-ui/src/app/services/rest/abstract-name-filter-service.ts @@ -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 { @@ -24,17 +29,22 @@ export abstract class AbstractNameFilterService< return this.list(page, pageSize, sortField, sortReverse, params) } - bulk_update_permissions( + bulk_edit_objects( objects: Array, - permissions: { owner: number; set_permissions: PermissionsObject }, - merge: boolean + operation: BulkEditObjectOperation, + permissions: { owner: number; set_permissions: PermissionsObject } = null, + merge: boolean = null ): Observable { - return this.http.post(`${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(`${this.baseUrl}bulk_edit_objects/`, params) } } diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 6135915d9..7de16a988 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -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, diff --git a/src-ui/src/environments/environment.ts b/src-ui/src/environments/environment.ts index fccb8927c..18715e90f 100644 --- a/src-ui/src/environments/environment.ts +++ b/src-ui/src/environments/environment.ts @@ -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', diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index e712d4b59..5fa104640 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -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 diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index e894cae90..3b38f2b5f 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -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") diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py index 851608f37..92e47a1ed 100644 --- a/src/documents/tests/test_api_permissions.py +++ b/src/documents/tests/test_api_permissions.py @@ -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, }, ), diff --git a/src/documents/views.py b/src/documents/views.py index 7a037c27d..3be7f4ec6 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -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): diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 94400c8dd..a9792db9f 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -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: diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 74f6fc108..0419b8e66 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -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(