Support update vs create

This commit is contained in:
shamoon 2025-07-02 12:33:36 -07:00
parent de9cc7420d
commit a7cdecd5a2
No known key found for this signature in database
7 changed files with 96 additions and 22 deletions

View File

@ -41,14 +41,14 @@
<button class="btn btn-sm btn-dark text-danger" (click)="remove(i); $event.stopPropagation()" title="Delete page" i18n-title> <button class="btn btn-sm btn-dark text-danger" (click)="remove(i); $event.stopPropagation()" title="Delete page" i18n-title>
<i-bs name="trash"></i-bs> <i-bs name="trash"></i-bs>
</button> </button>
<button class="btn btn-sm btn-dark" (click)="toggleSplit(i); $event.stopPropagation()" title="Split document here" i18n-title> <button class="btn btn-sm btn-dark" (click)="toggleSplit(i); $event.stopPropagation()" title="Add / remove document split here" i18n-title>
<i-bs name="scissors"></i-bs> <i-bs name="scissors"></i-bs>
</button> </button>
</div> </div>
</div> </div>
<div class="border-end border-bottom bg-light py-1 px-2 document-check z-10"> <div class="border-end border-bottom bg-light py-1 px-2 document-check z-10">
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="page{{i}}" [checked]="p.selected" (click)="toggleSelection(i); $event.stopPropagation()"> <input type="checkbox" class="form-check-input" id="page{{i}}" [checked]="p.selected" (click)="toggleSelection(i); $event.stopPropagation()">
<label class="form-check-label" for="page{{i}}"></label> <label class="form-check-label" for="page{{i}}"></label>
</div> </div>
</div> </div>
@ -68,11 +68,31 @@
} }
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer flex-column">
<div class="form-check form-switch me-auto"> <div class="d-flex w-100 justify-content-between align-items-center">
<input class="form-check-input" type="checkbox" id="deleteSwitch" [(ngModel)]="deleteOriginal"> <div class="btn-group" role="group">
<label class="form-check-label" for="deleteSwitch" i18n>Delete original after edit</label> <input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="EditMode.Create" id="editModeCreate" name="editmode">
<label for="editModeCreate" class="btn btn-outline-primary btn-sm">
<i-bs name="plus"></i-bs>
<span class="form-check-label ms-1" i18n>Create new document(s)</span>
</label>
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="EditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
<i-bs name="pencil"></i-bs>
<span class="form-check-label ms-2" i18n>Update existing document</span>
</label>
</div>
@if (editMode === EditMode.Create) {
<div class="form-check ms-3">
<input class="form-check-input" type="checkbox" id="copyMeta" [(ngModel)]="includeMetadata">
<label class="form-check-label" for="copyMeta" i18n>Copy metadata</label>
</div>
<div class="form-check ms-3">
<input class="form-check-input" type="checkbox" id="deleteOriginal" [(ngModel)]="deleteOriginal">
<label class="form-check-label" for="deleteOriginal" i18n>Delete original</label>
</div>
}
<button type="button" class="btn ms-auto me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
</div> </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> </div>

View File

@ -19,6 +19,11 @@ interface PageOperation {
loaded?: boolean loaded?: boolean
} }
enum EditMode {
Update = 'update',
Create = 'create',
}
@Component({ @Component({
selector: 'pngx-pdf-editor', selector: 'pngx-pdf-editor',
templateUrl: './pdf-editor.component.html', templateUrl: './pdf-editor.component.html',
@ -31,13 +36,18 @@ interface PageOperation {
], ],
}) })
export class PDFEditorComponent extends ConfirmDialogComponent { export class PDFEditorComponent extends ConfirmDialogComponent {
public EditMode = EditMode
private documentService = inject(DocumentService) private documentService = inject(DocumentService)
activeModal = inject(NgbActiveModal) activeModal: NgbActiveModal = inject(NgbActiveModal)
documentID: number documentID: number
pages: PageOperation[] = [] pages: PageOperation[] = []
totalPages = 0 totalPages = 0
deleteOriginal = false editMode: EditMode = EditMode.Create
deleteOriginal: boolean = false
updateDocument: boolean = false
includeMetadata: boolean = true
get pdfSrc(): string { get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID) return this.documentService.getPreviewUrl(this.documentID)
@ -76,6 +86,10 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
toggleSplit(i: number) { toggleSplit(i: number) {
this.pages[i].splitAfter = !this.pages[i].splitAfter this.pages[i].splitAfter = !this.pages[i].splitAfter
if (this.pages[i].splitAfter) {
// force create mode
this.editMode = EditMode.Create
}
} }
selectAll() { selectAll() {
@ -94,6 +108,10 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
return this.pages.some((p) => p.selected) return this.pages.some((p) => p.selected)
} }
hasSplit(): boolean {
return this.pages.some((p) => p.splitAfter)
}
drop(event: CdkDragDrop<PageOperation[]>) { drop(event: CdkDragDrop<PageOperation[]>) {
moveItemInArray(this.pages, event.previousIndex, event.currentIndex) moveItemInArray(this.pages, event.previousIndex, event.currentIndex)
} }

View File

@ -1159,7 +1159,8 @@ describe('DocumentDetailComponent', () => {
method: 'edit_pdf', method: 'edit_pdf',
parameters: { parameters: {
operations: [{ page: 1, rotate: 0, doc: 0 }], operations: [{ page: 1, rotate: 0, doc: 0 }],
delete_original: false, update_document: false,
include_metadata: true,
}, },
}) })
req.flush(true) req.flush(true)

View File

@ -1350,7 +1350,8 @@ export class DocumentDetailComponent
this.documentsService this.documentsService
.bulkEdit([this.document.id], 'edit_pdf', { .bulkEdit([this.document.id], 'edit_pdf', {
operations: modal.componentInstance.getOperations(), operations: modal.componentInstance.getOperations(),
delete_original: modal.componentInstance.deleteOriginal, update_document: modal.componentInstance.updateDocument,
include_metadata: modal.componentInstance.includeMetadata,
}) })
.pipe(first(), takeUntil(this.unsubscribeNotifier)) .pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({ .subscribe({

View File

@ -502,6 +502,8 @@ def edit_pdf(
operations: list[dict], operations: list[dict],
*, *,
delete_original: bool = False, delete_original: bool = False,
update_document: bool = False,
include_metadata: bool = True,
user: User | None = None, user: User | None = None,
) -> Literal["OK"]: ) -> Literal["OK"]:
""" """
@ -533,9 +535,25 @@ def edit_pdf(
if op.get("rotate"): if op.get("rotate"):
dst.pages[-1].rotate(op["rotate"], relative=True) dst.pages[-1].rotate(op["rotate"], relative=True)
if update_document:
if len(pdf_docs) != 1:
logger.error(
"Update requested but multiple output documents specified",
)
return "ERROR"
pdf = pdf_docs[0]
pdf.remove_unreferenced_resources()
pdf.save(doc.source_path)
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
doc.page_count = len(pdf.pages)
doc.save()
update_document_content_maybe_archive_file.delay(document_id=doc.id)
else:
consume_tasks = [] consume_tasks = []
overrides: DocumentMetadataOverrides = ( overrides = (
DocumentMetadataOverrides().from_document(doc) DocumentMetadataOverrides().from_document(doc)
if include_metadata
else DocumentMetadataOverrides()
) )
if user is not None: if user is not None:
overrides.owner_id = user.id overrides.owner_id = user.id
@ -564,6 +582,7 @@ def edit_pdf(
except Exception as e: except Exception as e:
logger.exception(f"Error editing document {doc.id}: {e}") logger.exception(f"Error editing document {doc.id}: {e}")
return "ERROR"
return "OK" return "OK"

View File

@ -1537,11 +1537,23 @@ class BulkEditSerializer(
raise serializers.ValidationError("rotate must be an integer") raise serializers.ValidationError("rotate must be an integer")
if "doc" in op and not isinstance(op["doc"], int): if "doc" in op and not isinstance(op["doc"], int):
raise serializers.ValidationError("doc must be an integer") raise serializers.ValidationError("doc must be an integer")
if "delete_original" in parameters: if "update_document" in parameters:
if not isinstance(parameters["delete_original"], bool): if not isinstance(parameters["update_document"], bool):
raise serializers.ValidationError("delete_original must be a boolean") raise serializers.ValidationError("update_document must be a boolean")
else: else:
parameters["delete_original"] = False parameters["update_document"] = False
if "include_metadata" in parameters:
if not isinstance(parameters["include_metadata"], bool):
raise serializers.ValidationError("include_metadata must be a boolean")
else:
parameters["include_metadata"] = True
if parameters["update_document"]:
max_idx = max(op.get("doc", 0) for op in parameters["operations"])
if max_idx > 0:
raise serializers.ValidationError(
"update_document only allowed with a single output document",
)
def validate(self, attrs): def validate(self, attrs):
method = attrs["method"] method = attrs["method"]

View File

@ -1368,17 +1368,21 @@ 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"]) or (method == bulk_edit.edit_pdf and parameters["update_document"])
): ):
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, bulk_edit.edit_pdf] and (
and not user.has_perm( method in [bulk_edit.split, bulk_edit.merge]
"documents.add_document", or (
method == bulk_edit.edit_pdf
and not parameters["update_document"]
)
) )
and not user.has_perm("documents.add_document")
): ):
has_perms = False has_perms = False
@ -1391,7 +1395,6 @@ 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")
): ):