mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Enhancement: mergeable bulk edit permissions (#5508)
This commit is contained in:
parent
8f3ab2791b
commit
454098630b
68
docs/api.md
68
docs/api.md
@ -330,6 +330,64 @@ granted). You can pass the parameter `full_perms=true` to API calls to view the
|
|||||||
full permissions of objects in a format that mirrors the `set_permissions`
|
full permissions of objects in a format that mirrors the `set_permissions`
|
||||||
parameter above.
|
parameter above.
|
||||||
|
|
||||||
|
## Bulk Editing
|
||||||
|
|
||||||
|
The API supports various bulk-editing operations which are executed asynchronously.
|
||||||
|
|
||||||
|
### Documents
|
||||||
|
|
||||||
|
For bulk operations on documents, use the endpoint `/api/bulk_edit/` which accepts
|
||||||
|
a json payload of the format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"documents": [LIST_OF_DOCUMENT_IDS],
|
||||||
|
"method": METHOD, // see below
|
||||||
|
"parameters": args // see below
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The following methods are supported:
|
||||||
|
|
||||||
|
- `set_correspondent`
|
||||||
|
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
|
||||||
|
- `set_document_type`
|
||||||
|
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
|
||||||
|
- `set_storage_path`
|
||||||
|
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
|
||||||
|
- `add_tag`
|
||||||
|
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||||
|
- `remove_tag`
|
||||||
|
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||||
|
- `modify_tags`
|
||||||
|
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||||
|
- `delete`
|
||||||
|
- No `parameters` required
|
||||||
|
- `redo_ocr`
|
||||||
|
- No `parameters` required
|
||||||
|
- `set_permissions`
|
||||||
|
- Requires `parameters`:
|
||||||
|
- `"permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
||||||
|
- `"owner": OWNER_ID or null`
|
||||||
|
- `"merge": true or false` (defaults to false)
|
||||||
|
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||||
|
removing them) or be merged with existing permissions.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objects": [LIST_OF_OBJECT_IDS],
|
||||||
|
"object_type": "tags", "correspondents", "document_types" or "storage_paths"
|
||||||
|
"owner": OWNER_ID // optional
|
||||||
|
"permissions": { "view": { "users": [] ... }, "change": { ... } }, // (see 'set_permissions' format above)
|
||||||
|
"merge": true / false // defaults to false, see above
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## API Versioning
|
## API Versioning
|
||||||
|
|
||||||
The REST API is versioned since Paperless-ngx 1.3.0.
|
The REST API is versioned since Paperless-ngx 1.3.0.
|
||||||
@ -386,3 +444,13 @@ Initial API version.
|
|||||||
color to use for a specific tag, which is either black or white
|
color to use for a specific tag, which is either black or white
|
||||||
depending on the brightness of `Tag.color`.
|
depending on the brightness of `Tag.color`.
|
||||||
- Removed field `Tag.colour`.
|
- Removed field `Tag.colour`.
|
||||||
|
|
||||||
|
#### Version 3
|
||||||
|
|
||||||
|
- Permissions endpoints have been added.
|
||||||
|
- The format of the `/api/ui_settings/` has changed.
|
||||||
|
|
||||||
|
#### Version 4
|
||||||
|
|
||||||
|
- Consumption templates were refactored to workflows and API endpoints
|
||||||
|
changed as such.
|
||||||
|
@ -620,7 +620,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">23</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
||||||
@ -2438,7 +2438,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
||||||
<context context-type="linenumber">23</context>
|
<context context-type="linenumber">26</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@ -2505,7 +2505,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
||||||
<context context-type="linenumber">22</context>
|
<context context-type="linenumber">25</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||||
@ -3923,6 +3923,13 @@
|
|||||||
<context context-type="linenumber">15</context>
|
<context context-type="linenumber">15</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="7940755769131903278" datatype="html">
|
||||||
|
<source>Merge with existing permissions</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">14</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="7062872617520618723" datatype="html">
|
<trans-unit id="7062872617520618723" datatype="html">
|
||||||
<source>Set permissions</source>
|
<source>Set permissions</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -3937,11 +3944,18 @@
|
|||||||
<context context-type="linenumber">33</context>
|
<context context-type="linenumber">33</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8283439432608484491" datatype="html">
|
<trans-unit id="347498040201588614" datatype="html">
|
||||||
<source>Note that permissions set here will override any existing permissions</source>
|
<source>Existing owner, user and group permissions will be merged with these settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3434726483516379481" datatype="html">
|
||||||
|
<source>Any and all existing owner, user and group permissions will be replaced.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
|
||||||
|
<context context-type="linenumber">75</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5947558132119506443" datatype="html">
|
<trans-unit id="5947558132119506443" datatype="html">
|
||||||
@ -6100,18 +6114,18 @@
|
|||||||
<source>Permissions updated</source>
|
<source>Permissions updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
|
||||||
<context context-type="linenumber">211</context>
|
<context context-type="linenumber">212</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4639647950943944112" datatype="html">
|
<trans-unit id="4639647950943944112" datatype="html">
|
||||||
<source>Error updating permissions</source>
|
<source>Error updating permissions</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
|
||||||
<context context-type="linenumber">215</context>
|
<context context-type="linenumber">217</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
||||||
<context context-type="linenumber">300</context>
|
<context context-type="linenumber">301</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4010735610815226758" datatype="html">
|
<trans-unit id="4010735610815226758" datatype="html">
|
||||||
@ -6277,7 +6291,7 @@
|
|||||||
<source>Permissions updated successfully</source>
|
<source>Permissions updated successfully</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
||||||
<context context-type="linenumber">293</context>
|
<context context-type="linenumber">294</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5101757640976222639" datatype="html">
|
<trans-unit id="5101757640976222639" datatype="html">
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
|
<div [ngClass]="{'align-items-center': horizontal, 'd-flex': horizontal}">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||||
@if (horizontal) {
|
@if (horizontal) {
|
||||||
|
@ -5,12 +5,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
@if (!object && message) {
|
|
||||||
<p class="mb-3" [innerHTML]="message | safeHtml"></p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
|
<div class="form-group">
|
||||||
|
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mt-4">
|
||||||
|
<div class="offset-lg-3 row">
|
||||||
|
<pngx-input-switch i18n-title title="Merge with existing permissions" [horizontal]="true" [hint]="hint" formControlName="merge"></pngx-input-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -20,5 +23,5 @@
|
|||||||
<span class="visually-hidden" i18n>Loading...</span>
|
<span class="visually-hidden" i18n>Loading...</span>
|
||||||
}
|
}
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
|
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button>
|
<button type="button" class="btn btn-primary" (click)="confirm()" [disabled]="!buttonsEnabled" i18n>Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ import { NgSelectModule } from '@ng-select/ng-select'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
|
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
|
||||||
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
|
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
|
||||||
|
import { SwitchComponent } from '../input/switch/switch.component'
|
||||||
|
|
||||||
const set_permissions = {
|
const set_permissions = {
|
||||||
owner: 10,
|
owner: 10,
|
||||||
@ -37,6 +38,7 @@ describe('PermissionsDialogComponent', () => {
|
|||||||
PermissionsDialogComponent,
|
PermissionsDialogComponent,
|
||||||
SafeHtmlPipe,
|
SafeHtmlPipe,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
|
SwitchComponent,
|
||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
PermissionsUserComponent,
|
PermissionsUserComponent,
|
||||||
PermissionsGroupComponent,
|
PermissionsGroupComponent,
|
||||||
@ -112,4 +114,23 @@ describe('PermissionsDialogComponent', () => {
|
|||||||
expect(component.title).toEqual(`Edit permissions for ${obj.name}`)
|
expect(component.title).toEqual(`Edit permissions for ${obj.name}`)
|
||||||
expect(component.permissions).toEqual(set_permissions)
|
expect(component.permissions).toEqual(set_permissions)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should toggle hint based on object existence (if editing) or merge flag', () => {
|
||||||
|
component.form.get('merge').setValue(true)
|
||||||
|
expect(component.hint.includes('Existing')).toBeTruthy()
|
||||||
|
component.form.get('merge').setValue(false)
|
||||||
|
expect(component.hint.includes('will be replaced')).toBeTruthy()
|
||||||
|
component.object = {}
|
||||||
|
expect(component.hint).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should emit permissions and merge flag on confirm', () => {
|
||||||
|
const confirmSpy = jest.spyOn(component.confirmClicked, 'emit')
|
||||||
|
component.form.get('permissions_form').setValue(set_permissions)
|
||||||
|
component.confirm()
|
||||||
|
expect(confirmSpy).toHaveBeenCalledWith({
|
||||||
|
permissions: set_permissions,
|
||||||
|
merge: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -32,6 +32,7 @@ export class PermissionsDialogComponent {
|
|||||||
this.o = o
|
this.o = o
|
||||||
this.title = $localize`Edit permissions for ` + o['name']
|
this.title = $localize`Edit permissions for ` + o['name']
|
||||||
this.form.patchValue({
|
this.form.patchValue({
|
||||||
|
merge: true,
|
||||||
permissions_form: {
|
permissions_form: {
|
||||||
owner: o.owner,
|
owner: o.owner,
|
||||||
set_permissions: o.permissions,
|
set_permissions: o.permissions,
|
||||||
@ -43,8 +44,9 @@ export class PermissionsDialogComponent {
|
|||||||
return this.o
|
return this.o
|
||||||
}
|
}
|
||||||
|
|
||||||
form = new FormGroup({
|
public form = new FormGroup({
|
||||||
permissions_form: new FormControl(),
|
permissions_form: new FormControl(),
|
||||||
|
merge: new FormControl(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
buttonsEnabled: boolean = true
|
buttonsEnabled: boolean = true
|
||||||
@ -66,11 +68,21 @@ export class PermissionsDialogComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
get hint(): string {
|
||||||
message =
|
if (this.object) return null
|
||||||
$localize`Note that permissions set here will override any existing permissions`
|
return this.form.get('merge').value
|
||||||
|
? $localize`Existing owner, user and group permissions will be merged with these settings.`
|
||||||
|
: $localize`Any and all existing owner, user and group permissions will be replaced.`
|
||||||
|
}
|
||||||
|
|
||||||
cancelClicked() {
|
cancelClicked() {
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.confirmClicked.emit({
|
||||||
|
permissions: this.permissions,
|
||||||
|
merge: this.form.get('merge').value,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
|
|||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||||
|
|
||||||
const selectionData: SelectionData = {
|
const selectionData: SelectionData = {
|
||||||
selected_tags: [
|
selected_tags: [
|
||||||
@ -81,6 +82,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
SelectComponent,
|
SelectComponent,
|
||||||
PermissionsGroupComponent,
|
PermissionsGroupComponent,
|
||||||
PermissionsUserComponent,
|
PermissionsUserComponent,
|
||||||
|
SwitchComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
@ -851,7 +853,18 @@ describe('BulkEditorComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
component.setPermissions()
|
component.setPermissions()
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
modal.componentInstance.confirmClicked.next()
|
const perms = {
|
||||||
|
permissions: {
|
||||||
|
view_users: [],
|
||||||
|
change_users: [],
|
||||||
|
view_groups: [],
|
||||||
|
change_groups: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
modal.componentInstance.confirmClicked.emit({
|
||||||
|
permissions: perms,
|
||||||
|
merge: true,
|
||||||
|
})
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
@ -859,7 +872,10 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect(req.request.body).toEqual({
|
expect(req.request.body).toEqual({
|
||||||
documents: [3, 4],
|
documents: [3, 4],
|
||||||
method: 'set_permissions',
|
method: 'set_permissions',
|
||||||
parameters: undefined,
|
parameters: {
|
||||||
|
permissions: perms.permissions,
|
||||||
|
merge: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
httpTestingController.match(
|
httpTestingController.match(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
@ -540,9 +540,14 @@ export class BulkEditorComponent
|
|||||||
let modal = this.modalService.open(PermissionsDialogComponent, {
|
let modal = this.modalService.open(PermissionsDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
modal.componentInstance.confirmClicked.subscribe((permissions) => {
|
modal.componentInstance.confirmClicked.subscribe(
|
||||||
modal.componentInstance.buttonsEnabled = false
|
({ permissions, merge }) => {
|
||||||
this.executeBulkOperation(modal, 'set_permissions', permissions)
|
modal.componentInstance.buttonsEnabled = false
|
||||||
})
|
this.executeBulkOperation(modal, 'set_permissions', {
|
||||||
|
...permissions,
|
||||||
|
merge,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import { TagsComponent } from '../../common/input/tags/tags.component'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||||
|
|
||||||
const mailAccounts = [
|
const mailAccounts = [
|
||||||
{ id: 1, name: 'account1' },
|
{ id: 1, name: 'account1' },
|
||||||
@ -82,6 +83,7 @@ describe('MailComponent', () => {
|
|||||||
PermissionsGroupComponent,
|
PermissionsGroupComponent,
|
||||||
PermissionsDialogComponent,
|
PermissionsDialogComponent,
|
||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
|
SwitchComponent,
|
||||||
],
|
],
|
||||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||||
imports: [
|
imports: [
|
||||||
@ -267,11 +269,11 @@ describe('MailComponent', () => {
|
|||||||
rulePatchSpy.mockReturnValueOnce(
|
rulePatchSpy.mockReturnValueOnce(
|
||||||
throwError(() => new Error('error saving perms'))
|
throwError(() => new Error('error saving perms'))
|
||||||
)
|
)
|
||||||
dialog.confirmClicked.emit(perms)
|
dialog.confirmClicked.emit({ permissions: perms, merge: true })
|
||||||
expect(rulePatchSpy).toHaveBeenCalled()
|
expect(rulePatchSpy).toHaveBeenCalled()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toHaveBeenCalled()
|
||||||
rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule))
|
rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule))
|
||||||
dialog.confirmClicked.emit(perms)
|
dialog.confirmClicked.emit({ permissions: perms, merge: true })
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated')
|
expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated')
|
||||||
|
|
||||||
modalService.dismissAll()
|
modalService.dismissAll()
|
||||||
@ -299,8 +301,7 @@ describe('MailComponent', () => {
|
|||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
let dialog = modal.componentInstance as PermissionsDialogComponent
|
let dialog = modal.componentInstance as PermissionsDialogComponent
|
||||||
expect(dialog.object).toEqual(mailAccounts[0])
|
expect(dialog.object).toEqual(mailAccounts[0])
|
||||||
dialog = modal.componentInstance as PermissionsDialogComponent
|
dialog.confirmClicked.emit({ permissions: perms, merge: true })
|
||||||
dialog.confirmClicked.emit(perms)
|
|
||||||
expect(accountPatchSpy).toHaveBeenCalled()
|
expect(accountPatchSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -200,22 +200,27 @@ export class MailComponent
|
|||||||
const dialog: PermissionsDialogComponent =
|
const dialog: PermissionsDialogComponent =
|
||||||
modal.componentInstance as PermissionsDialogComponent
|
modal.componentInstance as PermissionsDialogComponent
|
||||||
dialog.object = object
|
dialog.object = object
|
||||||
modal.componentInstance.confirmClicked.subscribe((permissions) => {
|
modal.componentInstance.confirmClicked.subscribe(
|
||||||
modal.componentInstance.buttonsEnabled = false
|
({ permissions, merge }) => {
|
||||||
const service: AbstractPaperlessService<MailRule | MailAccount> =
|
modal.componentInstance.buttonsEnabled = false
|
||||||
'account' in object ? this.mailRuleService : this.mailAccountService
|
const service: AbstractPaperlessService<MailRule | MailAccount> =
|
||||||
object.owner = permissions['owner']
|
'account' in object ? this.mailRuleService : this.mailAccountService
|
||||||
object['set_permissions'] = permissions['set_permissions']
|
object.owner = permissions['owner']
|
||||||
service.patch(object).subscribe({
|
object['set_permissions'] = permissions['set_permissions']
|
||||||
next: () => {
|
service.patch(object).subscribe({
|
||||||
this.toastService.showInfo($localize`Permissions updated`)
|
next: () => {
|
||||||
modal.close()
|
this.toastService.showInfo($localize`Permissions updated`)
|
||||||
},
|
modal.close()
|
||||||
error: (e) => {
|
},
|
||||||
this.toastService.showError($localize`Error updating permissions`, e)
|
error: (e) => {
|
||||||
},
|
this.toastService.showError(
|
||||||
})
|
$localize`Error updating permissions`,
|
||||||
})
|
e
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
userCanEdit(obj: ObjectWithPermissions): boolean {
|
userCanEdit(obj: ObjectWithPermissions): boolean {
|
||||||
|
@ -264,13 +264,19 @@ describe('ManagementListComponent', () => {
|
|||||||
throwError(() => new Error('error setting permissions'))
|
throwError(() => new Error('error setting permissions'))
|
||||||
)
|
)
|
||||||
const errorToastSpy = jest.spyOn(toastService, 'showError')
|
const errorToastSpy = jest.spyOn(toastService, 'showError')
|
||||||
modal.componentInstance.confirmClicked.emit()
|
modal.componentInstance.confirmClicked.emit({
|
||||||
|
permissions: {},
|
||||||
|
merge: true,
|
||||||
|
})
|
||||||
expect(bulkEditPermsSpy).toHaveBeenCalled()
|
expect(bulkEditPermsSpy).toHaveBeenCalled()
|
||||||
expect(errorToastSpy).toHaveBeenCalled()
|
expect(errorToastSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
const successToastSpy = jest.spyOn(toastService, 'showInfo')
|
const successToastSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
|
bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
|
||||||
modal.componentInstance.confirmClicked.emit()
|
modal.componentInstance.confirmClicked.emit({
|
||||||
|
permissions: {},
|
||||||
|
merge: true,
|
||||||
|
})
|
||||||
expect(bulkEditPermsSpy).toHaveBeenCalled()
|
expect(bulkEditPermsSpy).toHaveBeenCalled()
|
||||||
expect(successToastSpy).toHaveBeenCalled()
|
expect(successToastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
@ -279,12 +279,13 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
modal.componentInstance.confirmClicked.subscribe(
|
modal.componentInstance.confirmClicked.subscribe(
|
||||||
(permissions: { owner: number; set_permissions: PermissionsObject }) => {
|
({ permissions, merge }) => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.service
|
this.service
|
||||||
.bulk_update_permissions(
|
.bulk_update_permissions(
|
||||||
Array.from(this.selectedObjects),
|
Array.from(this.selectedObjects),
|
||||||
permissions
|
permissions,
|
||||||
|
merge
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
@ -53,10 +53,14 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
subscription = service
|
subscription = service
|
||||||
.bulk_update_permissions([1, 2], {
|
.bulk_update_permissions(
|
||||||
owner,
|
[1, 2],
|
||||||
set_permissions: permissions,
|
{
|
||||||
})
|
owner,
|
||||||
|
set_permissions: permissions,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}bulk_edit_object_perms/`
|
`${environment.apiBaseUrl}bulk_edit_object_perms/`
|
||||||
|
@ -26,13 +26,15 @@ export abstract class AbstractNameFilterService<
|
|||||||
|
|
||||||
bulk_update_permissions(
|
bulk_update_permissions(
|
||||||
objects: Array<number>,
|
objects: Array<number>,
|
||||||
permissions: { owner: number; set_permissions: PermissionsObject }
|
permissions: { owner: number; set_permissions: PermissionsObject },
|
||||||
|
merge: boolean
|
||||||
): Observable<string> {
|
): Observable<string> {
|
||||||
return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
|
return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
|
||||||
objects,
|
objects,
|
||||||
object_type: this.resourceName,
|
object_type: this.resourceName,
|
||||||
owner: permissions.owner,
|
owner: permissions.owner,
|
||||||
permissions: permissions.set_permissions,
|
permissions: permissions.set_permissions,
|
||||||
|
merge,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,13 +129,17 @@ def redo_ocr(doc_ids):
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
def set_permissions(doc_ids, set_permissions, owner=None):
|
def set_permissions(doc_ids, set_permissions, owner=None, merge=False):
|
||||||
qs = Document.objects.filter(id__in=doc_ids)
|
qs = Document.objects.filter(id__in=doc_ids)
|
||||||
|
|
||||||
qs.update(owner=owner)
|
if merge:
|
||||||
|
# If merging, only set owner for documents that don't have an owner
|
||||||
|
qs.filter(owner__isnull=True).update(owner=owner)
|
||||||
|
else:
|
||||||
|
qs.update(owner=owner)
|
||||||
|
|
||||||
for doc in qs:
|
for doc in qs:
|
||||||
set_permissions_for_object(set_permissions, doc)
|
set_permissions_for_object(permissions=set_permissions, object=doc, merge=merge)
|
||||||
|
|
||||||
affected_docs = [doc.id for doc in qs]
|
affected_docs = [doc.id for doc in qs]
|
||||||
|
|
||||||
|
@ -916,6 +916,8 @@ class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
|
|||||||
)
|
)
|
||||||
if "owner" in parameters and parameters["owner"] is not None:
|
if "owner" in parameters and parameters["owner"] is not None:
|
||||||
self._validate_owner(parameters["owner"])
|
self._validate_owner(parameters["owner"])
|
||||||
|
if "merge" not in parameters:
|
||||||
|
parameters["merge"] = False
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
method = attrs["method"]
|
method = attrs["method"]
|
||||||
@ -1258,6 +1260,12 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
merge = serializers.BooleanField(
|
||||||
|
default=False,
|
||||||
|
write_only=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def get_object_class(self, object_type):
|
def get_object_class(self, object_type):
|
||||||
object_class = None
|
object_class = None
|
||||||
if object_type == "tags":
|
if object_type == "tags":
|
||||||
|
110
src/documents/test_bulk_edit.py
Normal file
110
src/documents/test_bulk_edit.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
from guardian.shortcuts import assign_perm
|
||||||
|
from guardian.shortcuts import get_groups_with_perms
|
||||||
|
from guardian.shortcuts import get_users_with_perms
|
||||||
|
|
||||||
|
from documents.bulk_edit import set_permissions
|
||||||
|
from documents.models import Document
|
||||||
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestBulkEditPermissions(DirectoriesMixin, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.doc1 = Document.objects.create(checksum="A", title="A")
|
||||||
|
self.doc2 = Document.objects.create(checksum="B", title="B")
|
||||||
|
self.doc3 = Document.objects.create(checksum="C", title="C")
|
||||||
|
|
||||||
|
self.owner = User.objects.create(username="test_owner")
|
||||||
|
self.user1 = User.objects.create(username="user1")
|
||||||
|
self.user2 = User.objects.create(username="user2")
|
||||||
|
self.group1 = Group.objects.create(name="group1")
|
||||||
|
self.group2 = Group.objects.create(name="group2")
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.bulk_update_documents.delay")
|
||||||
|
def test_set_permissions(self, m):
|
||||||
|
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
|
||||||
|
|
||||||
|
assign_perm("view_document", self.group1, self.doc1)
|
||||||
|
|
||||||
|
permissions = {
|
||||||
|
"view": {
|
||||||
|
"users": [self.user1.id, self.user2.id],
|
||||||
|
"groups": [self.group2.id],
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [self.user1.id],
|
||||||
|
"groups": [self.group2.id],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
set_permissions(
|
||||||
|
doc_ids,
|
||||||
|
set_permissions=permissions,
|
||||||
|
owner=self.owner,
|
||||||
|
merge=False,
|
||||||
|
)
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
|
self.assertEqual(Document.objects.filter(owner=self.owner).count(), 3)
|
||||||
|
self.assertEqual(Document.objects.filter(id__in=doc_ids).count(), 3)
|
||||||
|
|
||||||
|
users_with_perms = get_users_with_perms(
|
||||||
|
self.doc1,
|
||||||
|
)
|
||||||
|
self.assertEqual(users_with_perms.count(), 2)
|
||||||
|
|
||||||
|
# group1 should be replaced by group2
|
||||||
|
groups_with_perms = get_groups_with_perms(
|
||||||
|
self.doc1,
|
||||||
|
)
|
||||||
|
self.assertEqual(groups_with_perms.count(), 1)
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.bulk_update_documents.delay")
|
||||||
|
def test_set_permissions_merge(self, m):
|
||||||
|
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
|
||||||
|
|
||||||
|
self.doc1.owner = self.user1
|
||||||
|
self.doc1.save()
|
||||||
|
|
||||||
|
assign_perm("view_document", self.user1, self.doc1)
|
||||||
|
assign_perm("view_document", self.group1, self.doc1)
|
||||||
|
|
||||||
|
permissions = {
|
||||||
|
"view": {
|
||||||
|
"users": [self.user2.id],
|
||||||
|
"groups": [self.group2.id],
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [self.user2.id],
|
||||||
|
"groups": [self.group2.id],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
set_permissions(
|
||||||
|
doc_ids,
|
||||||
|
set_permissions=permissions,
|
||||||
|
owner=self.owner,
|
||||||
|
merge=True,
|
||||||
|
)
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
|
# when merge is true owner doesn't get replaced if its not empty
|
||||||
|
self.assertEqual(Document.objects.filter(owner=self.owner).count(), 2)
|
||||||
|
self.assertEqual(Document.objects.filter(id__in=doc_ids).count(), 3)
|
||||||
|
|
||||||
|
# merge of user1 which was pre-existing and user2
|
||||||
|
users_with_perms = get_users_with_perms(
|
||||||
|
self.doc1,
|
||||||
|
)
|
||||||
|
self.assertEqual(users_with_perms.count(), 2)
|
||||||
|
|
||||||
|
# group1 should be merged by group2
|
||||||
|
groups_with_perms = get_groups_with_perms(
|
||||||
|
self.doc1,
|
||||||
|
)
|
||||||
|
self.assertEqual(groups_with_perms.count(), 2)
|
@ -765,6 +765,58 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
|||||||
self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
|
self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
|
||||||
self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
|
self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
|
def test_set_permissions_merge(self, m):
|
||||||
|
m.return_value = "OK"
|
||||||
|
user1 = User.objects.create(username="user1")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
permissions = {
|
||||||
|
"view": {
|
||||||
|
"users": [user1.id, user2.id],
|
||||||
|
"groups": None,
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [user1.id],
|
||||||
|
"groups": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "set_permissions",
|
||||||
|
"parameters": {"set_permissions": permissions},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
m.assert_called()
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
self.assertEqual(kwargs["merge"], False)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "set_permissions",
|
||||||
|
"parameters": {"set_permissions": permissions, "merge": True},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
m.assert_called()
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
self.assertEqual(kwargs["merge"], True)
|
||||||
|
|
||||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
def test_insufficient_permissions_ownership(self, m):
|
def test_insufficient_permissions_ownership(self, m):
|
||||||
"""
|
"""
|
||||||
|
@ -700,8 +700,8 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
user = User.objects.create_superuser(username="temp_admin")
|
self.temp_admin = User.objects.create_superuser(username="temp_admin")
|
||||||
self.client.force_authenticate(user=user)
|
self.client.force_authenticate(user=self.temp_admin)
|
||||||
|
|
||||||
self.t1 = Tag.objects.create(name="t1")
|
self.t1 = Tag.objects.create(name="t1")
|
||||||
self.t2 = Tag.objects.create(name="t2")
|
self.t2 = Tag.objects.create(name="t2")
|
||||||
@ -822,6 +822,79 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3)
|
self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3)
|
||||||
|
|
||||||
|
def test_bulk_object_set_permissions_merge(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_object_perms API endpoint is called with merge=True or merge=False (default)
|
||||||
|
THEN:
|
||||||
|
- Permissions and / or owner are replaced or merged, depending on the merge flag
|
||||||
|
"""
|
||||||
|
permissions = {
|
||||||
|
"view": {
|
||||||
|
"users": [self.user1.id, self.user2.id],
|
||||||
|
"groups": [],
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [self.user1.id],
|
||||||
|
"groups": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assign_perm("view_tag", self.user3, self.t1)
|
||||||
|
self.t1.owner = self.user3
|
||||||
|
self.t1.save()
|
||||||
|
|
||||||
|
# merge=True
|
||||||
|
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,
|
||||||
|
"permissions": permissions,
|
||||||
|
"merge": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.t1.refresh_from_db()
|
||||||
|
self.t2.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
# user3 should still be owner of t1 since was set prior
|
||||||
|
self.assertEqual(self.t1.owner, self.user3)
|
||||||
|
# user1 should now be owner of t2 since it didn't have an owner
|
||||||
|
self.assertEqual(self.t2.owner, self.user1)
|
||||||
|
|
||||||
|
# user1 should be added
|
||||||
|
self.assertIn(self.user1, get_users_with_perms(self.t1))
|
||||||
|
# user3 should be preserved
|
||||||
|
self.assertIn(self.user3, get_users_with_perms(self.t1))
|
||||||
|
|
||||||
|
# merge=False (default)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_object_perms/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id, self.t2.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"permissions": permissions,
|
||||||
|
"merge": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
# user1 should be added
|
||||||
|
self.assertIn(self.user1, get_users_with_perms(self.t1))
|
||||||
|
# user3 should be removed
|
||||||
|
self.assertNotIn(self.user3, get_users_with_perms(self.t1))
|
||||||
|
|
||||||
def test_bulk_edit_object_permissions_insufficient_perms(self):
|
def test_bulk_edit_object_permissions_insufficient_perms(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
@ -1385,6 +1385,7 @@ class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
|||||||
object_class = serializer.get_object_class(object_type)
|
object_class = serializer.get_object_class(object_type)
|
||||||
permissions = serializer.validated_data.get("permissions")
|
permissions = serializer.validated_data.get("permissions")
|
||||||
owner = serializer.validated_data.get("owner")
|
owner = serializer.validated_data.get("owner")
|
||||||
|
merge = serializer.validated_data.get("merge")
|
||||||
|
|
||||||
if not user.is_superuser:
|
if not user.is_superuser:
|
||||||
objs = object_class.objects.filter(pk__in=object_ids)
|
objs = object_class.objects.filter(pk__in=object_ids)
|
||||||
@ -1396,12 +1397,21 @@ class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
|||||||
try:
|
try:
|
||||||
qs = object_class.objects.filter(id__in=object_ids)
|
qs = object_class.objects.filter(id__in=object_ids)
|
||||||
|
|
||||||
if "owner" in serializer.validated_data:
|
# if merge is true, we dont want to remove the owner
|
||||||
qs.update(owner=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)
|
||||||
|
|
||||||
if "permissions" in serializer.validated_data:
|
if "permissions" in serializer.validated_data:
|
||||||
for obj in qs:
|
for obj in qs:
|
||||||
set_permissions_for_object(permissions, obj)
|
set_permissions_for_object(
|
||||||
|
permissions=permissions,
|
||||||
|
object=obj,
|
||||||
|
merge=merge,
|
||||||
|
)
|
||||||
|
|
||||||
return Response({"result": "OK"})
|
return Response({"result": "OK"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user