mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Support update vs create
This commit is contained in:
		@@ -41,14 +41,14 @@
 | 
			
		||||
            <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>
 | 
			
		||||
            </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>
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="border-end border-bottom bg-light py-1 px-2 document-check z-10">
 | 
			
		||||
          <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>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -68,11 +68,31 @@
 | 
			
		||||
    }
 | 
			
		||||
  </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 class="modal-footer flex-column">
 | 
			
		||||
  <div class="d-flex w-100 justify-content-between align-items-center">
 | 
			
		||||
    <div class="btn-group" role="group">
 | 
			
		||||
      <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>
 | 
			
		||||
  <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>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,11 @@ interface PageOperation {
 | 
			
		||||
  loaded?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum EditMode {
 | 
			
		||||
  Update = 'update',
 | 
			
		||||
  Create = 'create',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'pngx-pdf-editor',
 | 
			
		||||
  templateUrl: './pdf-editor.component.html',
 | 
			
		||||
@@ -31,13 +36,18 @@ interface PageOperation {
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class PDFEditorComponent extends ConfirmDialogComponent {
 | 
			
		||||
  public EditMode = EditMode
 | 
			
		||||
 | 
			
		||||
  private documentService = inject(DocumentService)
 | 
			
		||||
  activeModal = inject(NgbActiveModal)
 | 
			
		||||
  activeModal: NgbActiveModal = inject(NgbActiveModal)
 | 
			
		||||
 | 
			
		||||
  documentID: number
 | 
			
		||||
  pages: PageOperation[] = []
 | 
			
		||||
  totalPages = 0
 | 
			
		||||
  deleteOriginal = false
 | 
			
		||||
  editMode: EditMode = EditMode.Create
 | 
			
		||||
  deleteOriginal: boolean = false
 | 
			
		||||
  updateDocument: boolean = false
 | 
			
		||||
  includeMetadata: boolean = true
 | 
			
		||||
 | 
			
		||||
  get pdfSrc(): string {
 | 
			
		||||
    return this.documentService.getPreviewUrl(this.documentID)
 | 
			
		||||
@@ -76,6 +86,10 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
 | 
			
		||||
 | 
			
		||||
  toggleSplit(i: number) {
 | 
			
		||||
    this.pages[i].splitAfter = !this.pages[i].splitAfter
 | 
			
		||||
    if (this.pages[i].splitAfter) {
 | 
			
		||||
      // force create mode
 | 
			
		||||
      this.editMode = EditMode.Create
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectAll() {
 | 
			
		||||
@@ -94,6 +108,10 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
 | 
			
		||||
    return this.pages.some((p) => p.selected)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hasSplit(): boolean {
 | 
			
		||||
    return this.pages.some((p) => p.splitAfter)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  drop(event: CdkDragDrop<PageOperation[]>) {
 | 
			
		||||
    moveItemInArray(this.pages, event.previousIndex, event.currentIndex)
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1175,7 +1175,8 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
      method: 'edit_pdf',
 | 
			
		||||
      parameters: {
 | 
			
		||||
        operations: [{ page: 1, rotate: 0, doc: 0 }],
 | 
			
		||||
        delete_original: false,
 | 
			
		||||
        update_document: false,
 | 
			
		||||
        include_metadata: true,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
    req.flush(true)
 | 
			
		||||
 
 | 
			
		||||
@@ -1363,7 +1363,8 @@ export class DocumentDetailComponent
 | 
			
		||||
        this.documentsService
 | 
			
		||||
          .bulkEdit([this.document.id], 'edit_pdf', {
 | 
			
		||||
            operations: modal.componentInstance.getOperations(),
 | 
			
		||||
            delete_original: modal.componentInstance.deleteOriginal,
 | 
			
		||||
            update_document: modal.componentInstance.updateDocument,
 | 
			
		||||
            include_metadata: modal.componentInstance.includeMetadata,
 | 
			
		||||
          })
 | 
			
		||||
          .pipe(first(), takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
          .subscribe({
 | 
			
		||||
 
 | 
			
		||||
@@ -502,6 +502,8 @@ def edit_pdf(
 | 
			
		||||
    operations: list[dict],
 | 
			
		||||
    *,
 | 
			
		||||
    delete_original: bool = False,
 | 
			
		||||
    update_document: bool = False,
 | 
			
		||||
    include_metadata: bool = True,
 | 
			
		||||
    user: User | None = None,
 | 
			
		||||
) -> Literal["OK"]:
 | 
			
		||||
    """
 | 
			
		||||
@@ -533,9 +535,25 @@ def edit_pdf(
 | 
			
		||||
                if op.get("rotate"):
 | 
			
		||||
                    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 = []
 | 
			
		||||
            overrides: DocumentMetadataOverrides = (
 | 
			
		||||
            overrides = (
 | 
			
		||||
                DocumentMetadataOverrides().from_document(doc)
 | 
			
		||||
                if include_metadata
 | 
			
		||||
                else DocumentMetadataOverrides()
 | 
			
		||||
            )
 | 
			
		||||
            if user is not None:
 | 
			
		||||
                overrides.owner_id = user.id
 | 
			
		||||
@@ -564,6 +582,7 @@ def edit_pdf(
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.exception(f"Error editing document {doc.id}: {e}")
 | 
			
		||||
        return "ERROR"
 | 
			
		||||
 | 
			
		||||
    return "OK"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1537,11 +1537,23 @@ class BulkEditSerializer(
 | 
			
		||||
                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")
 | 
			
		||||
        if "update_document" in parameters:
 | 
			
		||||
            if not isinstance(parameters["update_document"], bool):
 | 
			
		||||
                raise serializers.ValidationError("update_document must be a boolean")
 | 
			
		||||
        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):
 | 
			
		||||
        method = attrs["method"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1375,17 +1375,21 @@ class BulkEditView(PassUserMixin):
 | 
			
		||||
                    method in [bulk_edit.merge, bulk_edit.split]
 | 
			
		||||
                    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
 | 
			
		||||
 | 
			
		||||
            # check global add permissions for methods that create documents
 | 
			
		||||
            if (
 | 
			
		||||
                has_perms
 | 
			
		||||
                and method in [bulk_edit.split, bulk_edit.merge, bulk_edit.edit_pdf]
 | 
			
		||||
                and not user.has_perm(
 | 
			
		||||
                    "documents.add_document",
 | 
			
		||||
                and (
 | 
			
		||||
                    method in [bulk_edit.split, bulk_edit.merge]
 | 
			
		||||
                    or (
 | 
			
		||||
                        method == bulk_edit.edit_pdf
 | 
			
		||||
                        and not parameters["update_document"]
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                and not user.has_perm("documents.add_document")
 | 
			
		||||
            ):
 | 
			
		||||
                has_perms = False
 | 
			
		||||
 | 
			
		||||
@@ -1398,7 +1402,6 @@ class BulkEditView(PassUserMixin):
 | 
			
		||||
                        method in [bulk_edit.merge, bulk_edit.split]
 | 
			
		||||
                        and parameters["delete_originals"]
 | 
			
		||||
                    )
 | 
			
		||||
                    or (method == bulk_edit.edit_pdf and parameters["delete_original"])
 | 
			
		||||
                )
 | 
			
		||||
                and not user.has_perm("documents.delete_document")
 | 
			
		||||
            ):
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user