}
getErrorText(error: any) {
- const text: string = error.error?.detail ?? error.error ?? ''
+ let text: string = error.error?.detail ?? error.error ?? ''
+ if (typeof text === 'object') text = JSON.stringify(text)
return `${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`
}
}
<pngx-page-header title="{{ typeNamePlural | titlecase }}">
+ <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
+ <svg class="sidebaricon" fill="currentColor">
+ <use xlink:href="assets/bootstrap-icons.svg#x"/>
+ </svg> <ng-container i18n>Clear selection</ng-container>
+ </button>
+ <button type="button" class="btn btn-sm btn-outline-primary me-5" (click)="setPermissions()" [disabled]="!userOwnsAll || selectedObjects.size === 0">
+ <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
+ <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
+ </svg> <ng-container i18n>Permissions</ng-container>
+ </button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button>
</pngx-page-header>
<table class="table table-striped align-middle border shadow-sm">
<thead>
<tr>
+ <th scope="col">
+ <div class="form-check">
+ <input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
+ <label class="form-check-label" for="all-objects"></label>
+ </div>
+ </th>
<th scope="col" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" class="d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<ng-container i18n>Loading...</ng-container>
</td>
</tr>
- <tr *ngFor="let object of data">
+ <tr *ngFor="let object of data" (click)="toggleSelected(object, $event); $event.stopPropagation();">
+ <td>
+ <div class="form-check">
+ <input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
+ <label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
+ </div>
+ </td>
<td scope="row">{{ object.name }}</td>
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
<td scope="row">{{ object.document_count }}</td>
</div>
</div>
<div class="btn-group d-none d-sm-block">
- <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
+ <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg> <ng-container i18n>Documents</ng-container>
</button>
- <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
+ <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg> <ng-container i18n>Edit</ng-container>
</button>
- <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
+ <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg> <ng-container i18n>Delete</ng-container>
</tbody>
</table>
-<div class="d-flex" *ngIf="!isLoading">
- <div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div>
+<div class="d-flex mb-2" *ngIf="!isLoading">
+ <div *ngIf="collectionSize > 0">
+ <ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
+ <ng-container *ngIf="selectedObjects.size > 0"> ({{selectedObjects.size}} selected)</ng-container>
+ </div>
<ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div>
import { MATCH_AUTO } from 'src/app/data/matching-model'
import { MATCH_NONE } from 'src/app/data/matching-model'
import { MATCH_LITERAL } from 'src/app/data/matching-model'
+import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
const tags: PaperlessTag[] = [
{
IfPermissionsDirective,
SafeHtmlPipe,
ConfirmDialogComponent,
+ PermissionsDialogComponent,
],
providers: [
{
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData')
- const createButton = fixture.debugElement.queryAll(By.css('button'))[0]
+ const createButton = fixture.debugElement.queryAll(By.css('button'))[2]
createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData')
- const editButton = fixture.debugElement.queryAll(By.css('button'))[3]
+ const editButton = fixture.debugElement.queryAll(By.css('button'))[5]
editButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const deleteSpy = jest.spyOn(tagService, 'delete')
const reloadSpy = jest.spyOn(component, 'reloadData')
- const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4]
+ const deleteButton = fixture.debugElement.queryAll(By.css('button'))[6]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
it('should support quick filter for objects', () => {
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
- const filterButton = fixture.debugElement.queryAll(By.css('button'))[2]
+ const filterButton = fixture.debugElement.queryAll(By.css('button'))[4]
filterButton.triggerEventHandler('click')
expect(qfSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
sortable.triggerEventHandler('click')
expect(reloadSpy).toHaveBeenCalled()
})
+
+ it('should support toggle all items in view', () => {
+ expect(component.selectedObjects.size).toEqual(0)
+ const toggleAllSpy = jest.spyOn(component, 'toggleAll')
+ const checkButton = fixture.debugElement.queryAll(
+ By.css('input.form-check-input')
+ )[0]
+ checkButton.nativeElement.dispatchEvent(new Event('click'))
+ checkButton.nativeElement.checked = true
+ checkButton.nativeElement.dispatchEvent(new Event('click'))
+ expect(toggleAllSpy).toHaveBeenCalled()
+ expect(component.selectedObjects.size).toEqual(tags.length)
+ })
+
+ it('should support bulk edit permissions', () => {
+ const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_update_permissions')
+ component.toggleSelected(tags[0])
+ component.toggleSelected(tags[1])
+ component.toggleSelected(tags[2])
+ component.toggleSelected(tags[2]) // uncheck, for coverage
+ const selected = new Set([tags[0].id, tags[1].id])
+ expect(component.selectedObjects).toEqual(selected)
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ fixture.detectChanges()
+ component.setPermissions()
+ expect(modal).not.toBeUndefined()
+
+ // fail first
+ bulkEditPermsSpy.mockReturnValueOnce(
+ throwError(() => new Error('error setting permissions'))
+ )
+ const errorToastSpy = jest.spyOn(toastService, 'showError')
+ modal.componentInstance.confirmClicked.emit()
+ expect(bulkEditPermsSpy).toHaveBeenCalled()
+ expect(errorToastSpy).toHaveBeenCalled()
+
+ const successToastSpy = jest.spyOn(toastService, 'showInfo')
+ bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
+ modal.componentInstance.confirmClicked.emit()
+ expect(bulkEditPermsSpy).toHaveBeenCalled()
+ expect(successToastSpy).toHaveBeenCalled()
+ })
})
ViewChildren,
} from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { Subject, Subscription } from 'rxjs'
+import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'
import {
MatchingModel,
MATCH_NONE,
} from 'src/app/data/matching-model'
import { ObjectWithId } from 'src/app/data/object-with-id'
-import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
+import {
+ ObjectWithPermissions,
+ PermissionsObject,
+} from 'src/app/data/object-with-permissions'
import {
SortableDirective,
SortEvent,
import { AbstractNameFilterService } 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 {
- EditDialogComponent,
- EditDialogMode,
-} from '../../common/edit-dialog/edit-dialog.component'
+import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
+import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
export interface ManagementListColumn {
key: string
private unsubscribeNotifier: Subject<any> = new Subject()
private _nameFilter: string
+ public selectedObjects: Set<number> = new Set()
+
ngOnInit(): void {
this.reloadData()
object
)
}
+
+ get userOwnsAll(): boolean {
+ let ownsAll: boolean = true
+ const objects = this.data.filter((o) => this.selectedObjects.has(o.id))
+ ownsAll = objects.every((o) =>
+ this.permissionsService.currentUserOwnsObject(o)
+ )
+ return ownsAll
+ }
+
+ toggleAll(event: PointerEvent) {
+ if ((event.target as HTMLInputElement).checked) {
+ this.selectedObjects = new Set(this.data.map((o) => o.id))
+ } else {
+ this.clearSelection()
+ }
+ }
+
+ clearSelection() {
+ this.selectedObjects.clear()
+ }
+
+ toggleSelected(object) {
+ this.selectedObjects.has(object.id)
+ ? this.selectedObjects.delete(object.id)
+ : this.selectedObjects.add(object.id)
+ }
+
+ setPermissions() {
+ let modal = this.modalService.open(PermissionsDialogComponent, {
+ backdrop: 'static',
+ })
+ modal.componentInstance.confirmClicked.subscribe(
+ (permissions: { owner: number; set_permissions: PermissionsObject }) => {
+ modal.componentInstance.buttonsEnabled = false
+ this.service
+ .bulk_update_permissions(
+ Array.from(this.selectedObjects),
+ permissions
+ )
+ .subscribe({
+ next: () => {
+ modal.close()
+ this.toastService.showInfo(
+ $localize`Permissions updated successfully`
+ )
+ this.reloadData()
+ },
+ error: (error) => {
+ modal.componentInstance.buttonsEnabled = true
+ this.toastService.showError(
+ $localize`Error updating permissions`,
+ error
+ )
+ },
+ })
+ }
+ )
+ }
}
<tbody>
<ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize">
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
- <th>
+ <td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
- </th>
+ </td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'">
import { ObjectWithId } from './object-with-id'
-import { PaperlessUser } from './paperless-user'
export interface PermissionsObject {
view: {
expect(req.request.method).toEqual('GET')
req.flush([])
})
+
+ test('should call appropriate api endpoint for bulk permissions edit', () => {
+ const owner = 3
+ const permissions = {
+ view: {
+ users: [],
+ groups: [3],
+ },
+ change: {
+ users: [12, 13],
+ groups: [],
+ },
+ }
+ subscription = service
+ .bulk_update_permissions([1, 2], {
+ owner,
+ set_permissions: permissions,
+ })
+ .subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}bulk_edit_object_perms/`
+ )
+ expect(req.request.method).toEqual('POST')
+ req.flush([])
+ })
})
beforeEach(() => {
import { ObjectWithId } from 'src/app/data/object-with-id'
import { AbstractPaperlessService } from './abstract-paperless-service'
+import { PermissionsObject } from 'src/app/data/object-with-permissions'
+import { Observable } from 'rxjs'
export abstract class AbstractNameFilterService<
T extends ObjectWithId,
}
return this.list(page, pageSize, sortField, sortReverse, params)
}
+
+ bulk_update_permissions(
+ objects: Array<number>,
+ permissions: { owner: number; set_permissions: PermissionsObject }
+ ): Observable<string> {
+ return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
+ objects,
+ object_type: this.resourceName,
+ owner: permissions.owner,
+ permissions: permissions.set_permissions,
+ })
+ }
}
def create(self, validated_data):
validated_data["slug"] = get_random_string(50)
return super().create(validated_data)
+
+
+class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissionsMixin):
+ objects = serializers.ListField(
+ required=True,
+ allow_empty=False,
+ label="Objects",
+ write_only=True,
+ child=serializers.IntegerField(),
+ )
+
+ object_type = serializers.ChoiceField(
+ choices=[
+ "tags",
+ "correspondents",
+ "document_types",
+ "storage_paths",
+ ],
+ label="Object Type",
+ write_only=True,
+ )
+
+ owner = serializers.PrimaryKeyRelatedField(
+ queryset=User.objects.all(),
+ required=False,
+ allow_null=True,
+ )
+
+ permissions = serializers.DictField(
+ label="Set permissions",
+ allow_empty=False,
+ required=False,
+ write_only=True,
+ )
+
+ def get_object_class(self, object_type):
+ object_class = None
+ if object_type == "tags":
+ object_class = Tag
+ elif object_type == "correspondents":
+ object_class = Correspondent
+ elif object_type == "document_types":
+ object_class = DocumentType
+ elif object_type == "storage_paths":
+ object_class = StoragePath
+ return object_class
+
+ def _validate_objects(self, objects, object_type):
+ if not isinstance(objects, list):
+ raise serializers.ValidationError("objects must be a list")
+ if not all(isinstance(i, int) for i in objects):
+ raise serializers.ValidationError("objects must be a list of integers")
+ object_class = self.get_object_class(object_type)
+ count = object_class.objects.filter(id__in=objects).count()
+ if not count == len(objects):
+ raise serializers.ValidationError(
+ "Some ids in objects don't exist or were specified twice.",
+ )
+ return objects
+
+ def _validate_permissions(self, permissions):
+ self.validate_set_permissions(
+ permissions,
+ )
+
+ def validate(self, attrs):
+ object_type = attrs["object_type"]
+ objects = attrs["objects"]
+ permissions = attrs["permissions"] if "permissions" in attrs else None
+
+ self._validate_objects(objects, object_type)
+ if permissions is not None:
+ self._validate_permissions(permissions)
+
+ return attrs
from django.utils import timezone
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_perms
+from guardian.shortcuts import get_users_with_perms
from rest_framework import status
from rest_framework.test import APITestCase
from whoosh.writing import AsyncWriter
returned_group1 = Group.objects.get(pk=group1.pk)
self.assertEqual(returned_group1.name, "Updated Name 1")
+
+
+class TestBulkEditObjectPermissions(APITestCase):
+ def setUp(self):
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+
+ 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_object_set_permissions(self):
+ """
+ GIVEN:
+ - Existing objects
+ WHEN:
+ - bulk_edit_object_perms API endpoint is called
+ THEN:
+ - Permissions and / or owner are changed
+ """
+ permissions = {
+ "view": {
+ "users": [self.user1.id, self.user2.id],
+ "groups": [],
+ },
+ "change": {
+ "users": [self.user1.id],
+ "groups": [],
+ },
+ }
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id],
+ "object_type": "tags",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.t1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.c1.id],
+ "object_type": "correspondents",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.c1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.dt1.id],
+ "object_type": "document_types",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.dt1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.sp1.id],
+ "object_type": "storage_paths",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.sp1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id],
+ "object_type": "tags",
+ "owner": self.user3.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.sp1.id],
+ "object_type": "storage_paths",
+ "owner": self.user3.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3)
+
+ def test_bulk_edit_object_permissions_insufficient_perms(self):
+ """
+ GIVEN:
+ - Objects owned by user other than logged in user
+ WHEN:
+ - bulk_edit_object_perms API endpoint is called
+ THEN:
+ - User is not able to change permissions
+ """
+ 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_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id],
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+ self.assertEqual(response.content, b"Insufficient permissions")
+
+ def test_bulk_edit_object_permissions_validation(self):
+ """
+ GIVEN:
+ - Existing objects
+ WHEN:
+ - bulk_edit_object_perms API endpoint is called with invalid params
+ THEN:
+ - Validation fails
+ """
+ # not a list
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": self.t1.id,
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # not a list of ints
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": ["one"],
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # duplicates
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id, self.t1.id],
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # not a valid object type
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [1],
+ "object_type": "madeup",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
from documents.permissions import PaperlessObjectPermissions
from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import has_perms_owner_aware
+from documents.permissions import set_permissions_for_object
from documents.tasks import consume_file
from paperless import version
from paperless.db import GnuPG
from .parsers import parse_date_generator
from .serialisers import AcknowledgeTasksViewSerializer
from .serialisers import BulkDownloadSerializer
+from .serialisers import BulkEditObjectPermissionsSerializer
from .serialisers import BulkEditSerializer
from .serialisers import CorrespondentSerializer
from .serialisers import DocumentListSerializer
)
response["Content-Disposition"] = content_disposition
return response
+
+
+class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
+ permission_classes = (IsAuthenticated,)
+ serializer_class = BulkEditObjectPermissionsSerializer
+ parser_classes = (parsers.JSONParser,)
+
+ def post(self, request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ user = self.request.user
+ 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")
+
+ 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 "owner" in serializer.validated_data:
+ qs.update(owner=owner)
+
+ if "permissions" in serializer.validated_data:
+ for obj in qs:
+ set_permissions_for_object(permissions, obj)
+
+ 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.",
+ )
from documents.views import AcknowledgeTasksView
from documents.views import BulkDownloadView
+from documents.views import BulkEditObjectPermissionsView
from documents.views import BulkEditView
from documents.views import CorrespondentViewSet
from documents.views import DocumentTypeViewSet
name="mail_accounts_test",
),
path("token/", views.obtain_auth_token),
+ re_path(
+ "^bulk_edit_object_perms/",
+ BulkEditObjectPermissionsView.as_view(),
+ name="bulk_edit_object_permissions",
+ ),
*api_router.urls,
],
),