mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-12 17:04:40 -05:00
Just save this
[ci skip]
This commit is contained in:
parent
1cd21d0f38
commit
d3644463cc
@ -0,0 +1,29 @@
|
|||||||
|
<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">{{ title }}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div cdkDropList (cdkDropListDropped)="drop($event)" cdkDropListOrientation="mixed" class="d-flex flex-wrap row-cols-5">
|
||||||
|
@for (p of pages; track p.page; let i = $index) {
|
||||||
|
<div class="page-item p-2" cdkDrag>
|
||||||
|
<div class="btn-group mb-1">
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="rotate(i)"><i-bs name="arrow-clockwise"></i-bs></button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="toggleSplit(i)"><i-bs name="scissors"></i-bs></button>
|
||||||
|
<button class="btn btn-sm btn-danger" (click)="remove(i)"><i-bs name="trash"></i-bs></button>
|
||||||
|
</div>
|
||||||
|
<div class="pdf-viewer-container w-100 mt-3">
|
||||||
|
<pdf-viewer [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false"></pdf-viewer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="form-check form-switch me-auto">
|
||||||
|
<input class="form-check-input" type="checkbox" id="deleteSwitch" [(ngModel)]="deleteOriginal">
|
||||||
|
<label class="form-check-label" for="deleteSwitch" i18n>Delete original after edit</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
|
||||||
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
|
||||||
|
</div>
|
@ -0,0 +1,9 @@
|
|||||||
|
.pdf-viewer-container {
|
||||||
|
background-color: gray;
|
||||||
|
height: 120px;
|
||||||
|
|
||||||
|
pdf-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { PDFEditorComponent } from './pdf-editor.component'
|
||||||
|
|
||||||
|
describe('PDFEditorComponent', () => {
|
||||||
|
let component: PDFEditorComponent
|
||||||
|
let fixture: ComponentFixture<PDFEditorComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PDFEditorComponent, NgxBootstrapIconsModule.pick(allIcons)],
|
||||||
|
providers: [
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: NgbActiveModal, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
fixture = TestBed.createComponent(PDFEditorComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rotate and reorder pages', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: false },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false },
|
||||||
|
]
|
||||||
|
component.rotate(0)
|
||||||
|
expect(component.pages[0].rotate).toBe(90)
|
||||||
|
component.drop({ previousIndex: 0, currentIndex: 1 } as any)
|
||||||
|
expect(component.pages[0].page).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
CdkDragDrop,
|
||||||
|
DragDropModule,
|
||||||
|
moveItemInArray,
|
||||||
|
} from '@angular/cdk/drag-drop'
|
||||||
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
||||||
|
|
||||||
|
interface PageOperation {
|
||||||
|
page: number
|
||||||
|
rotate: number
|
||||||
|
splitAfter: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-pdf-editor',
|
||||||
|
templateUrl: './pdf-editor.component.html',
|
||||||
|
styleUrl: './pdf-editor.component.scss',
|
||||||
|
imports: [
|
||||||
|
DragDropModule,
|
||||||
|
FormsModule,
|
||||||
|
PdfViewerModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PDFEditorComponent
|
||||||
|
extends ConfirmDialogComponent
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
|
private documentService = inject(DocumentService)
|
||||||
|
activeModal = inject(NgbActiveModal)
|
||||||
|
|
||||||
|
documentID: number
|
||||||
|
pages: PageOperation[] = []
|
||||||
|
totalPages = 0
|
||||||
|
deleteOriginal = false
|
||||||
|
|
||||||
|
get pdfSrc(): string {
|
||||||
|
return this.documentService.getPreviewUrl(this.documentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
|
||||||
|
pdfLoaded(pdf: PDFDocumentProxy) {
|
||||||
|
this.totalPages = pdf.numPages
|
||||||
|
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
|
||||||
|
page: i + 1,
|
||||||
|
rotate: 0,
|
||||||
|
splitAfter: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(i: number) {
|
||||||
|
this.pages[i].rotate = (this.pages[i].rotate + 90) % 360
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(i: number) {
|
||||||
|
this.pages.splice(i, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSplit(i: number) {
|
||||||
|
this.pages[i].splitAfter = !this.pages[i].splitAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(event: CdkDragDrop<PageOperation[]>) {
|
||||||
|
moveItemInArray(this.pages, event.previousIndex, event.currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
getOperations() {
|
||||||
|
const operations = this.pages.map((p, idx) => ({
|
||||||
|
page: p.page,
|
||||||
|
rotate: p.rotate,
|
||||||
|
doc: this.computeDocIndex(idx),
|
||||||
|
}))
|
||||||
|
return operations
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeDocIndex(index: number): number {
|
||||||
|
let docIndex = 0
|
||||||
|
for (let i = 0; i <= index; i++) {
|
||||||
|
if (this.pages[i].splitAfter && i < index) docIndex++
|
||||||
|
}
|
||||||
|
return docIndex
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,10 @@
|
|||||||
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
||||||
|
<i-bs name="pencil"></i-bs> <ng-container i18n>Edit PDF</ng-container>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="deletePages()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
|
<button ngbDropdownItem (click)="deletePages()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
|
||||||
<i-bs name="file-earmark-minus"></i-bs> <ng-container i18n>Delete page(s)</ng-container>
|
<i-bs name="file-earmark-minus"></i-bs> <ng-container i18n>Delete page(s)</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1219,6 +1219,29 @@ describe('DocumentDetailComponent', () => {
|
|||||||
req.flush(true)
|
req.flush(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support pdf editor', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
|
initNormally()
|
||||||
|
component.editPdf()
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
modal.componentInstance.documentID = doc.id
|
||||||
|
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
|
||||||
|
modal.componentInstance.confirm()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
|
)
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
documents: [doc.id],
|
||||||
|
method: 'edit_pdf',
|
||||||
|
parameters: {
|
||||||
|
operations: [{ page: 1, rotate: 0, doc: 0 }],
|
||||||
|
delete_original: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req.flush(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('should support keyboard shortcuts', () => {
|
it('should support keyboard shortcuts', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ import { TagsComponent } from '../common/input/tags/tags.component'
|
|||||||
import { TextComponent } from '../common/input/text/text.component'
|
import { TextComponent } from '../common/input/text/text.component'
|
||||||
import { UrlComponent } from '../common/input/url/url.component'
|
import { UrlComponent } from '../common/input/url/url.component'
|
||||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||||
|
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
||||||
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
||||||
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
@ -1417,6 +1418,45 @@ export class DocumentDetailComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editPdf() {
|
||||||
|
let modal = this.modalService.open(PDFEditorComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
size: 'xl',
|
||||||
|
scrollable: true,
|
||||||
|
})
|
||||||
|
modal.componentInstance.title = $localize`Edit PDF`
|
||||||
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
|
modal.componentInstance.documentID = this.document.id
|
||||||
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
|
this.documentsService
|
||||||
|
.bulkEdit([this.document.id], 'edit_pdf', {
|
||||||
|
operations: modal.componentInstance.getOperations(),
|
||||||
|
delete_original: modal.componentInstance.deleteOriginal,
|
||||||
|
})
|
||||||
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`PDF edit operation for "${this.document.title}" will begin in the background.`
|
||||||
|
)
|
||||||
|
modal.close()
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
if (modal) {
|
||||||
|
modal.componentInstance.buttonsEnabled = true
|
||||||
|
}
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error executing PDF edit operation`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
deletePages() {
|
deletePages() {
|
||||||
let modal = this.modalService.open(DeletePagesConfirmDialogComponent, {
|
let modal = this.modalService.open(DeletePagesConfirmDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
|
@ -497,6 +497,77 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def edit_pdf(
|
||||||
|
doc_ids: list[int],
|
||||||
|
operations: list[dict],
|
||||||
|
*,
|
||||||
|
delete_original: bool = False,
|
||||||
|
user: User | None = None,
|
||||||
|
) -> Literal["OK"]:
|
||||||
|
"""
|
||||||
|
Operations is a list of dictionaries describing the final PDF pages.
|
||||||
|
Each entry must contain the original page number in `page` and may
|
||||||
|
specify `rotate` in degrees and `doc` indicating the output
|
||||||
|
document index (for splitting). Pages omitted from the list are
|
||||||
|
discarded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Editing PDF of document {doc_ids[0]} with {len(operations)} operations",
|
||||||
|
)
|
||||||
|
doc = Document.objects.get(id=doc_ids[0])
|
||||||
|
import pikepdf
|
||||||
|
|
||||||
|
pdf_docs: list[pikepdf.Pdf] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pikepdf.open(doc.source_path) as src:
|
||||||
|
# prepare output documents
|
||||||
|
max_idx = max(op.get("doc", 0) for op in operations)
|
||||||
|
pdf_docs = [pikepdf.new() for _ in range(max_idx + 1)]
|
||||||
|
|
||||||
|
for op in operations:
|
||||||
|
dst = pdf_docs[op.get("doc", 0)]
|
||||||
|
page = src.pages[op["page"] - 1]
|
||||||
|
dst.pages.append(page)
|
||||||
|
if op.get("rotate"):
|
||||||
|
dst.pages[-1].rotate(op["rotate"], relative=True)
|
||||||
|
|
||||||
|
consume_tasks = []
|
||||||
|
overrides: DocumentMetadataOverrides = (
|
||||||
|
DocumentMetadataOverrides().from_document(doc)
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
overrides.owner_id = user.id
|
||||||
|
|
||||||
|
for idx, pdf in enumerate(pdf_docs, start=1):
|
||||||
|
filepath: Path = (
|
||||||
|
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||||
|
/ f"{doc.id}_edit_{idx}.pdf"
|
||||||
|
)
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
pdf.save(filepath)
|
||||||
|
consume_tasks.append(
|
||||||
|
consume_file.s(
|
||||||
|
ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=filepath,
|
||||||
|
),
|
||||||
|
overrides,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if delete_original:
|
||||||
|
chord(header=consume_tasks, body=delete.si([doc.id])).delay()
|
||||||
|
else:
|
||||||
|
group(consume_tasks).delay()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error editing document {doc.id}: {e}")
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
def reflect_doclinks(
|
def reflect_doclinks(
|
||||||
document: Document,
|
document: Document,
|
||||||
field: CustomField,
|
field: CustomField,
|
||||||
|
@ -1366,6 +1366,8 @@ class BulkEditSerializer(
|
|||||||
return bulk_edit.split
|
return bulk_edit.split
|
||||||
elif method == "delete_pages":
|
elif method == "delete_pages":
|
||||||
return bulk_edit.delete_pages
|
return bulk_edit.delete_pages
|
||||||
|
elif method == "edit_pdf":
|
||||||
|
return bulk_edit.edit_pdf
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError("Unsupported method.")
|
raise serializers.ValidationError("Unsupported method.")
|
||||||
|
|
||||||
@ -1520,6 +1522,26 @@ class BulkEditSerializer(
|
|||||||
else:
|
else:
|
||||||
parameters["archive_fallback"] = False
|
parameters["archive_fallback"] = False
|
||||||
|
|
||||||
|
def _validate_parameters_edit_pdf(self, parameters):
|
||||||
|
if "operations" not in parameters:
|
||||||
|
raise serializers.ValidationError("operations not specified")
|
||||||
|
if not isinstance(parameters["operations"], list):
|
||||||
|
raise serializers.ValidationError("operations must be a list")
|
||||||
|
for op in parameters["operations"]:
|
||||||
|
if not isinstance(op, dict):
|
||||||
|
raise serializers.ValidationError("invalid operation entry")
|
||||||
|
if "page" not in op or not isinstance(op["page"], int):
|
||||||
|
raise serializers.ValidationError("page must be an integer")
|
||||||
|
if "rotate" in op and not isinstance(op["rotate"], int):
|
||||||
|
raise serializers.ValidationError("rotate must be an integer")
|
||||||
|
if "doc" in op and not isinstance(op["doc"], int):
|
||||||
|
raise serializers.ValidationError("doc must be an integer")
|
||||||
|
if "delete_original" in parameters:
|
||||||
|
if not isinstance(parameters["delete_original"], bool):
|
||||||
|
raise serializers.ValidationError("delete_original must be a boolean")
|
||||||
|
else:
|
||||||
|
parameters["delete_original"] = False
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
method = attrs["method"]
|
method = attrs["method"]
|
||||||
parameters = attrs["parameters"]
|
parameters = attrs["parameters"]
|
||||||
@ -1554,6 +1576,12 @@ class BulkEditSerializer(
|
|||||||
self._validate_parameters_delete_pages(parameters)
|
self._validate_parameters_delete_pages(parameters)
|
||||||
elif method == bulk_edit.merge:
|
elif method == bulk_edit.merge:
|
||||||
self._validate_parameters_merge(parameters)
|
self._validate_parameters_merge(parameters)
|
||||||
|
elif method == bulk_edit.edit_pdf:
|
||||||
|
if len(attrs["documents"]) > 1:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Edit PDF method only supports one document",
|
||||||
|
)
|
||||||
|
self._validate_parameters_edit_pdf(parameters)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -1369,6 +1369,60 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn(b"pages must be a list of integers", response.content)
|
self.assertIn(b"pages must be a list of integers", response.content)
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
|
||||||
|
def test_edit_pdf(self, m):
|
||||||
|
self.setup_mock(m, "edit_pdf")
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
m.assert_called_once()
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
self.assertCountEqual(args[0], [self.doc2.id])
|
||||||
|
self.assertEqual(kwargs["operations"], [{"page": 1}])
|
||||||
|
self.assertEqual(kwargs["user"], self.user)
|
||||||
|
|
||||||
|
def test_edit_pdf_invalid_params(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"Edit PDF method only supports one document", response.content)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"operations not specified", response.content)
|
||||||
|
|
||||||
@override_settings(AUDIT_LOG_ENABLED=True)
|
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||||
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1314,6 +1314,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
"delete_pages": "checksum",
|
"delete_pages": "checksum",
|
||||||
"split": None,
|
"split": None,
|
||||||
"merge": None,
|
"merge": None,
|
||||||
|
"edit_pdf": None,
|
||||||
"reprocess": "checksum",
|
"reprocess": "checksum",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1332,6 +1333,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
if method in [
|
if method in [
|
||||||
bulk_edit.split,
|
bulk_edit.split,
|
||||||
bulk_edit.merge,
|
bulk_edit.merge,
|
||||||
|
bulk_edit.edit_pdf,
|
||||||
]:
|
]:
|
||||||
parameters["user"] = user
|
parameters["user"] = user
|
||||||
|
|
||||||
@ -1351,24 +1353,29 @@ class BulkEditView(PassUserMixin):
|
|||||||
|
|
||||||
# check ownership for methods that change original document
|
# check ownership for methods that change original document
|
||||||
if (
|
if (
|
||||||
has_perms
|
(
|
||||||
and method
|
has_perms
|
||||||
in [
|
and method
|
||||||
bulk_edit.set_permissions,
|
in [
|
||||||
bulk_edit.delete,
|
bulk_edit.set_permissions,
|
||||||
bulk_edit.rotate,
|
bulk_edit.delete,
|
||||||
bulk_edit.delete_pages,
|
bulk_edit.rotate,
|
||||||
]
|
bulk_edit.delete_pages,
|
||||||
) or (
|
bulk_edit.edit_pdf,
|
||||||
method in [bulk_edit.merge, bulk_edit.split]
|
]
|
||||||
and parameters["delete_originals"]
|
)
|
||||||
|
or (
|
||||||
|
method in [bulk_edit.merge, bulk_edit.split]
|
||||||
|
and parameters["delete_originals"]
|
||||||
|
)
|
||||||
|
or (method == bulk_edit.edit_pdf and parameters["delete_original"])
|
||||||
):
|
):
|
||||||
has_perms = user_is_owner_of_all_documents
|
has_perms = user_is_owner_of_all_documents
|
||||||
|
|
||||||
# check global add permissions for methods that create documents
|
# check global add permissions for methods that create documents
|
||||||
if (
|
if (
|
||||||
has_perms
|
has_perms
|
||||||
and method in [bulk_edit.split, bulk_edit.merge]
|
and method in [bulk_edit.split, bulk_edit.merge, bulk_edit.edit_pdf]
|
||||||
and not user.has_perm(
|
and not user.has_perm(
|
||||||
"documents.add_document",
|
"documents.add_document",
|
||||||
)
|
)
|
||||||
@ -1384,6 +1391,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
method in [bulk_edit.merge, bulk_edit.split]
|
method in [bulk_edit.merge, bulk_edit.split]
|
||||||
and parameters["delete_originals"]
|
and parameters["delete_originals"]
|
||||||
)
|
)
|
||||||
|
or (method == bulk_edit.edit_pdf and parameters["delete_original"])
|
||||||
)
|
)
|
||||||
and not user.has_perm("documents.delete_document")
|
and not user.has_perm("documents.delete_document")
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user