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>
 | 
					            <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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1175,7 +1175,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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1363,7 +1363,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({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1375,17 +1375,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1398,7 +1402,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")
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user