mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: mergeable bulk edit permissions (#5508)
This commit is contained in:
		
							
								
								
									
										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"> | ||||||
|  |     <div class="form-group"> | ||||||
|       <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> |       <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( | ||||||
|  |       ({ permissions, merge }) => { | ||||||
|         modal.componentInstance.buttonsEnabled = false |         modal.componentInstance.buttonsEnabled = false | ||||||
|       this.executeBulkOperation(modal, 'set_permissions', permissions) |         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,7 +200,8 @@ 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( | ||||||
|  |       ({ permissions, merge }) => { | ||||||
|         modal.componentInstance.buttonsEnabled = false |         modal.componentInstance.buttonsEnabled = false | ||||||
|         const service: AbstractPaperlessService<MailRule | MailAccount> = |         const service: AbstractPaperlessService<MailRule | MailAccount> = | ||||||
|           'account' in object ? this.mailRuleService : this.mailAccountService |           'account' in object ? this.mailRuleService : this.mailAccountService | ||||||
| @@ -212,10 +213,14 @@ export class MailComponent | |||||||
|             modal.close() |             modal.close() | ||||||
|           }, |           }, | ||||||
|           error: (e) => { |           error: (e) => { | ||||||
|           this.toastService.showError($localize`Error updating permissions`, 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( | ||||||
|  |           [1, 2], | ||||||
|  |           { | ||||||
|             owner, |             owner, | ||||||
|             set_permissions: permissions, |             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) | ||||||
|  |  | ||||||
|  |     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) |         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: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon