Bulk edit permissions

This commit is contained in:
Michael Shamoon committed 2022-12-08 02:03:50 -08:00
1 parent 211fbf0cf6
commit 6ece5240a5
12 files changed
+267 -99

No files matched your search

+2
View File
@@ -108,6 +108,7 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv' import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr' import localeTr from '@angular/common/locales/tr'
import localeZh from '@angular/common/locales/zh' import localeZh from '@angular/common/locales/zh'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
registerLocaleData(localeBe) registerLocaleData(localeBe)
registerLocaleData(localeCs) registerLocaleData(localeCs)
@@ -203,6 +204,7 @@ function initializeApp(settings: SettingsService) {
PermissionsGroupComponent, PermissionsGroupComponent,
IfOwnerDirective, IfOwnerDirective,
IfObjectPermissionsDirective, IfObjectPermissionsDirective,
PermissionsDialogComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core' import { Component, EventEmitter, Input, Output } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { interval, Subject, switchMap, take } from 'rxjs' import { interval, Subject, take } from 'rxjs'
@Component({ @Component({
selector: 'app-confirm-dialog', selector: 'app-confirm-dialog',
@@ -0,0 +1,29 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()">
</button>
</div>
<div class="modal-body">
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p>
<form [formGroup]="form">
<div formGroupName="set_permissions">
<h6 i18n>View</h6>
<div formGroupName="view">
<app-permissions-user type="view" formControlName="users"></app-permissions-user>
<app-permissions-group type="view" formControlName="groups"></app-permissions-group>
</div>
<h6 i18n>Edit</h6>
<div formGroupName="change">
<app-permissions-user type="change" formControlName="users"></app-permissions-user>
<app-permissions-group type="change" formControlName="groups"></app-permissions-group>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" i18n>Cancel</button>
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" i18n>Confirm</button>
</div>
@@ -0,0 +1,46 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessUser } from 'src/app/data/paperless-user'
@Component({
selector: 'app-permissions-dialog',
templateUrl: './permissions-dialog.component.html',
styleUrls: ['./permissions-dialog.component.scss'],
})
export class PermissionsDialogComponent implements OnInit {
constructor(public activeModal: NgbActiveModal) {}
@Output()
public confirmClicked = new EventEmitter()
@Input()
title = $localize`Set Permissions`
form = new FormGroup({
set_permissions: new FormGroup({
view: new FormGroup({
users: new FormControl([]),
groups: new FormControl([]),
}),
change: new FormGroup({
users: new FormControl([]),
groups: new FormControl([]),
}),
}),
})
get permissions() {
return this.form.value['set_permissions']
}
@Input()
message = $localize`Note that permissions set here will override any existing permissions`
ngOnInit(): void {}
cancelClicked() {
this.activeModal.close()
}
}
@@ -65,7 +65,13 @@
</div> </div>
</div> </div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> <div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
<div class="btn-group btn-group-sm me-2"> <div class="btn-toolbar me-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<div ngbDropdown class="me-2 d-flex"> <div ngbDropdown class="me-2 d-flex">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
@@ -26,6 +26,7 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
@Component({ @Component({
selector: 'app-bulk-editor', selector: 'app-bulk-editor',
@@ -397,4 +398,16 @@ export class BulkEditorComponent extends ComponentWithPermissions {
this.executeBulkOperation(modal, 'redo_ocr', {}) this.executeBulkOperation(modal, 'redo_ocr', {})
}) })
} }
setPermissions() {
let modal = this.modalService.open(PermissionsDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.confirmClicked.subscribe((permissions) => {
modal.componentInstance.buttonsEnabled = false
this.executeBulkOperation(modal, 'set_permissions', {
permissions,
})
})
}
} }
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol class="bi bi-123" viewBox="0 0 16 16" id="123"><path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75v.96z"/></symbol><symbol class="bi bi-activity" viewBox="0 0 16 16" id="activity"><path fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2z"/></symbol><symbol class="bi bi-alarm" viewBox="0 0 16 16" id="alarm"><path d="M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5z"/><path d="M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1h-3zm1.038 3.018a6.093 6.093 0 0 1 .924 0 6 6 0 1 1-.924 0zM0 3.5c0 .753.333 1.429.86 1.887A8.035 8.035 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5zM13.5 1c-.753 0-1.429.333-1.887.86a8.035 8.035 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1z"/></symbol><symbol class="bi bi-alarm-fill" viewBox="0 0 16 16" id="alarm-fill"><path d="M6 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H9v1.07a7.001 7.001 0 0 1 3.274 12.474l.601.602a.5.5 0 0 1-.707.708l-.746-.746A6.97 6.97 0 0 1 8 16a6.97 6.97 0 0 1-3.422-.892l-.746.746a.5.5 0 0 1-.707-.708l.602-.602A7.001 7.001 0 0 1 7 2.07V1h-.5A.5.5 0 0 1 6 .5zm2.5 5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5zM.86 5.387A2.5 2.5 0 1 1 4.387 1.86 8.035 8.035 0 0 0 .86 5.387zM11.613 1.86a2.5 2.5 0 1 1 3.527 3.527 8.035 8.035 0 0 0-3.527-3.527z"/></symbol><symbol class="bi bi-align-bottom" viewBox="0 0 16 16" id="align-bottom"><rect width="4" height="12" x="6" y="1" rx="1"/><path d="M1.5 14a.5.5 0 0 0 0 1v-1zm13 1a.5.5 0 0 0 0-1v1zm-13 0h13v-1h-13v1z"/></symbol><symbol class="bi bi-align-center" viewBox="0 0 16 16" id="align-center"><path d="M8 1a.5.5 0 0 1 .5.5V6h-1V1.5A.5.5 0 0 1 8 1zm0 14a.5.5 0 0 1-.5-.5V10h1v4.5a.5.5 0 0 1-.5.5zM2 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7z"/></symbol><symbol class="bi bi-align-end" viewBox="0 0 16 16" id="align-end"><path fill-rule="evenodd" d="M14.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 1 0v-13a.5.5 0 0 0-.5-.5z"/><path d="M13 7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7z"/></symbol><symbol class="bi bi-align-middle" viewBox="0 0 16 16" id="align-middle"><path d="M6 13a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v10zM1 8a.5.5 0 0 0 .5.5H6v-1H1.5A.5.5 0 0 0 1 8zm14 0a.5.5 0 0 1-.5.5H10v-1h4.5a.5.5 0 0 1 .5.5z"/></symbol><symbol class="bi bi-align-start" viewBox="0 0 16 16" id="align-start"><path fill-rule="evenodd" d="M1.5 1a.5.5 0 0 1 .5.5v13a.5.5 0 0 1-1 0v-13a.5.5 0 0 1 .5-.5z"/><path d="M3 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z"/></symbol><symbol class="bi bi-align-top" viewBox="0 0 16 16" id="align-top"><rect width="4" height="12" rx="1" transform="matrix(1 0 0 -1 6 15)"/><path d="M1.5 2a.5.5 0 0 1 0-1v1zm13-1a.5.5 0 0 1 0 1V1zm-13 0h13v1h-13V1z"/></symbol><symbol class="bi bi-alt" viewBox="0 0 16 16" id="alt"><path d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5zm10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5z"/></symbol><symbol class="bi bi-app" viewBox="0 0 16 16" id="app"><path d="M11 2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h6zM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5z"/></symbol><symbol class="bi bi-app-indicator" viewBox="0 0 16 16" id="app-indicator"><path d="M5.5 2A3.5 3.5 0 0 0 2 5.5v5A3.5 3.5 0 0 0 5.5 14h5a3.5 3.5 0 0 0 3.5-3.5V8a.5.5 0 0 1 1 0v2.5a4.5 4.5 0 0 1-4.5 4.5h-5A4.5 4.5 0 0 1 1 10.5v-5A4.5 4.5 0 0 1 5.5 1H8a.5.5 0 0 1 0 1H5.5z"/><path d="M16 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/></symbol><symbol class="bi bi-apple" viewBox="0 0 16 16" id="apple"><path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.56Line truncated <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol class="bi bi-0-circle" viewBox="0 0 16 16" id="0-circle"><path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99ZM8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8Z"/></symbol><symbol class="bi bi-0-circle-fill" viewBox="0 0 16 16" id="0-circle-fill"><path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99Z"/></symbol><symbol class="bi bi-0-square" viewBox="0 0 16 16" id="0-square"><path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99ZM8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2Zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2Z"/></symbol><symbol class="bi bi-0-square-fill" viewBox="0 0 16 16" id="0-square-fill"><path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/><path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2Zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99Z"/></symbol><symbol class="bi bi-1-circle" viewBox="0 0 16 16" id="1-circle"><path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8Zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383h1.312Z"/></symbol><symbol class="bi bi-1-circle-fill" viewBox="0 0 16 16" id="1-circle-fill"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312V4.002Z"/></symbol><symbol class="bi bi-1-square" viewBox="0 0 16 16" id="1-square"><path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383h1.312Z"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2Zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2Z"/></symbol><symbol class="bi bi-1-square-fill" viewBox="0 0 16 16" id="1-square-fill"><path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2Zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383h1.312Z"/></symbol><symbol class="bi bi-123" viewBox="0 0 16 16" id="123"><path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/></symbol><symbol class="bi bi-2-circle" viewBox="0 0 16 16" id="2-circle"><path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8Zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306Z"/></symbol><symbol class="bi bi-2-circle-fill" viewBox="0 0 16 16" id="2-circle-fill"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/></symbol><symbol class="bi bi-2-square" viewBox="0 0 16 16" id="2-square"><path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306Z"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2Zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 Line truncated

Before

Width:  |  Height:  |  Size: 861 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+13
View File
@@ -5,6 +5,7 @@ from documents.models import Correspondent
from documents.models import Document from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import StoragePath from documents.models import StoragePath
from documents.permissions import set_permissions_for_object
from documents.tasks import bulk_update_documents from documents.tasks import bulk_update_documents
from documents.tasks import update_document_archive_file from documents.tasks import update_document_archive_file
@@ -128,3 +129,15 @@ def redo_ocr(doc_ids):
) )
return "OK" return "OK"
def set_permissions(doc_ids, permissions):
qs = Document.objects.filter(id__in=doc_ids)
for doc in qs:
set_permissions_for_object(permissions, doc)
affected_docs = [doc.id for doc in qs]
bulk_update_documents.delay(document_ids=affected_docs)
return "OK"
+70
View File
@@ -1,3 +1,11 @@
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_users_with_perms
from guardian.shortcuts import remove_perm
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from rest_framework.permissions import DjangoObjectPermissions from rest_framework.permissions import DjangoObjectPermissions
@@ -31,3 +39,65 @@ class PaperlessObjectPermissions(DjangoObjectPermissions):
class PaperlessAdminPermissions(BasePermission): class PaperlessAdminPermissions(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.has_perm("admin.view_logentry") return request.user.has_perm("admin.view_logentry")
def get_groups_with_only_permission(obj, codename):
ctype = ContentType.objects.get_for_model(obj)
permission = Permission.objects.get(content_type=ctype, codename=codename)
group_object_perm_group_ids = (
GroupObjectPermission.objects.filter(
object_pk=obj.pk,
content_type=ctype,
)
.filter(permission=permission)
.values_list("group_id")
)
return Group.objects.filter(id__in=group_object_perm_group_ids).distinct()
def set_permissions_for_object(permissions, object):
print(permissions, object)
for action in permissions:
permission = f"{action}_{object.__class__.__name__.lower()}"
# users
users_to_add = User.objects.filter(id__in=permissions[action]["users"])
users_to_remove = get_users_with_perms(
object,
only_with_perms_in=[permission],
)
if len(users_to_add) > 0 and len(users_to_remove) > 0:
users_to_remove = users_to_remove.difference(users_to_add)
if len(users_to_remove) > 0:
for user in users_to_remove:
remove_perm(permission, user, object)
if len(users_to_add) > 0:
for user in users_to_add:
assign_perm(permission, user, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
user,
object,
)
# groups
groups_to_add = Group.objects.filter(id__in=permissions[action]["groups"])
groups_to_remove = get_groups_with_only_permission(
object,
permission,
)
if len(groups_to_add) > 0 and len(groups_to_remove) > 0:
groups_to_remove = groups_to_remove.difference(groups_to_add)
if len(groups_to_remove) > 0:
for group in groups_to_remove:
remove_perm(permission, group, object)
if len(groups_to_add) > 0:
for group in groups_to_add:
assign_perm(permission, group, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
group,
object,
)
+58 -96
View File
@@ -28,16 +28,13 @@ from .models import UiSettings
from .models import PaperlessTask from .models import PaperlessTask
from .parsers import is_mime_type_supported from .parsers import is_mime_type_supported
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm
from guardian.shortcuts import remove_perm
from guardian.shortcuts import get_users_with_perms from guardian.shortcuts import get_users_with_perms
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from documents.permissions import get_groups_with_only_permission
from documents.permissions import set_permissions_for_object
# https://www.django-rest-framework.org/api-guide/serializers/#example # https://www.django-rest-framework.org/api-guide/serializers/#example
@@ -85,56 +82,7 @@ class MatchingModelSerializer(serializers.ModelSerializer):
return match return match
def get_groups_with_only_permission(obj, codename): class SetPermissionsMixin:
ctype = ContentType.objects.get_for_model(obj)
permission = Permission.objects.get(content_type=ctype, codename=codename)
group_object_perm_group_ids = (
GroupObjectPermission.objects.filter(
object_pk=obj.pk,
content_type=ctype,
)
.filter(permission=permission)
.values_list("group_id")
)
return Group.objects.filter(id__in=group_object_perm_group_ids).distinct()
class OwnedObjectSerializer(serializers.ModelSerializer):
def get_permissions(self, obj):
view_codename = f"view_{obj.__class__.__name__.lower()}"
change_codename = f"change_{obj.__class__.__name__.lower()}"
return {
"view": {
"users": get_users_with_perms(
obj,
only_with_perms_in=[view_codename],
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=view_codename,
).values_list("id", flat=True),
},
"change": {
"users": get_users_with_perms(
obj,
only_with_perms_in=[change_codename],
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=change_codename,
).values_list("id", flat=True),
},
}
permissions = SerializerMethodField(read_only=True)
set_permissions = serializers.DictField(
label="Set permissions",
allow_empty=True,
required=False,
write_only=True,
)
def _validate_user_ids(self, user_ids): def _validate_user_ids(self, user_ids):
users = User.objects.none() users = User.objects.none()
if user_ids is not None: if user_ids is not None:
@@ -169,52 +117,55 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
if set_permissions is not None: if set_permissions is not None:
for action in permissions_dict: for action in permissions_dict:
users = set_permissions[action]["users"] users = set_permissions[action]["users"]
permissions_dict[action]["users"] = self._validate_user_ids(users) self._validate_user_ids(users)
groups = set_permissions[action]["groups"] groups = set_permissions[action]["groups"]
permissions_dict[action]["groups"] = self._validate_group_ids(groups) self._validate_group_ids(groups)
return permissions_dict return permissions_dict
def _set_permissions(self, permissions, object):
set_permissions_for_object(permissions, object)
class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None) self.user = kwargs.pop("user", None)
return super().__init__(*args, **kwargs) return super().__init__(*args, **kwargs)
def _set_permissions(self, permissions, object): def get_permissions(self, obj):
for action in permissions: view_codename = f"view_{obj.__class__.__name__.lower()}"
permission = f"{action}_{object.__class__.__name__.lower()}" change_codename = f"change_{obj.__class__.__name__.lower()}"
# users return {
users_to_add = permissions[action]["users"] "view": {
users_to_remove = get_users_with_perms( "users": get_users_with_perms(
object, obj,
only_with_perms_in=[permission], only_with_perms_in=[view_codename],
).difference(users_to_add) ).values_list("id", flat=True),
for user in users_to_remove: "groups": get_groups_with_only_permission(
remove_perm(permission, user, object) obj,
for user in users_to_add: codename=view_codename,
assign_perm(permission, user, object) ).values_list("id", flat=True),
if action == "change": },
# change gives view too "change": {
assign_perm( "users": get_users_with_perms(
f"view_{object.__class__.__name__.lower()}", obj,
user, only_with_perms_in=[change_codename],
object, ).values_list("id", flat=True),
) "groups": get_groups_with_only_permission(
# groups obj,
groups_to_add = permissions[action]["groups"] codename=change_codename,
groups_to_remove = get_groups_with_only_permission( ).values_list("id", flat=True),
object, },
permission, }
).difference(groups_to_add)
for group in groups_to_remove: permissions = SerializerMethodField(read_only=True)
remove_perm(permission, group, object)
for group in groups_to_add: set_permissions = serializers.DictField(
assign_perm(permission, group, object) label="Set permissions",
if action == "change": allow_empty=True,
# change gives view too required=False,
assign_perm( write_only=True,
f"view_{object.__class__.__name__.lower()}", )
group, # other methods in mixin
object,
)
def create(self, validated_data): def create(self, validated_data):
if self.user and ( if self.user and (
@@ -515,7 +466,7 @@ class DocumentListSerializer(serializers.Serializer):
return documents return documents
class BulkEditSerializer(DocumentListSerializer): class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
method = serializers.ChoiceField( method = serializers.ChoiceField(
choices=[ choices=[
@@ -527,6 +478,7 @@ class BulkEditSerializer(DocumentListSerializer):
"modify_tags", "modify_tags",
"delete", "delete",
"redo_ocr", "redo_ocr",
"set_permissions",
], ],
label="Method", label="Method",
write_only=True, write_only=True,
@@ -562,6 +514,8 @@ class BulkEditSerializer(DocumentListSerializer):
return bulk_edit.delete return bulk_edit.delete
elif method == "redo_ocr": elif method == "redo_ocr":
return bulk_edit.redo_ocr return bulk_edit.redo_ocr
elif method == "set_permissions":
return bulk_edit.set_permissions
else: else:
raise serializers.ValidationError("Unsupported method.") raise serializers.ValidationError("Unsupported method.")
@@ -625,6 +579,12 @@ class BulkEditSerializer(DocumentListSerializer):
else: else:
raise serializers.ValidationError("remove_tags not specified") raise serializers.ValidationError("remove_tags not specified")
def _validate_parameters_set_permissions(self, parameters):
if "permissions" in parameters:
self.validate_set_permissions(parameters["permissions"])
else:
raise serializers.ValidationError("permissions not specified")
def validate(self, attrs): def validate(self, attrs):
method = attrs["method"] method = attrs["method"]
@@ -640,6 +600,8 @@ class BulkEditSerializer(DocumentListSerializer):
self._validate_parameters_modify_tags(parameters) self._validate_parameters_modify_tags(parameters)
elif method == bulk_edit.set_storage_path: elif method == bulk_edit.set_storage_path:
self._validate_storage_path(parameters) self._validate_storage_path(parameters)
elif method == bulk_edit.set_permissions:
self._validate_parameters_set_permissions(parameters)
return attrs return attrs
+27
View File
@@ -41,6 +41,8 @@ from paperless import version
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from whoosh.writing import AsyncWriter from whoosh.writing import AsyncWriter
from guardian.shortcuts import get_users_with_perms
class TestDocumentApi(DirectoriesMixin, APITestCase): class TestDocumentApi(DirectoriesMixin, APITestCase):
def setUp(self): def setUp(self):
@@ -2329,6 +2331,31 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
], ],
) )
def test_set_permissions(self):
user1 = User.objects.create(username="user1")
user2 = User.objects.create(username="user2")
permissions = {
"view": {
"users": User.objects.filter(id__in=[user1.id, user2.id]),
"groups": Group.objects.none(),
},
"change": {
"users": User.objects.filter(id__in=[user1.id]),
"groups": Group.objects.none(),
},
}
bulk_edit.set_permissions(
[self.doc2.id, self.doc3.id],
permissions=permissions,
)
self.assertEqual(get_users_with_perms(self.doc2).count(), 2)
self.async_task.assert_called_once()
args, kwargs = self.async_task.call_args
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
class TestBulkDownload(DirectoriesMixin, APITestCase): class TestBulkDownload(DirectoriesMixin, APITestCase):
def setUp(self): def setUp(self):