]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: add switch to allow merging non-PDFs with archive version (#9305)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 5 Mar 2025 20:46:51 +0000 (12:46 -0800)
committerGitHub <noreply@github.com>
Wed, 5 Mar 2025 20:46:51 +0000 (20:46 +0000)
src-ui/messages.xlf
src-ui/src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html
src-ui/src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.ts
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
src/documents/bulk_edit.py
src/documents/serialisers.py
src/documents/tests/test_bulk_edit.py

index cd1a979503c6b1d573e96b56ab23629b3f26fab6..1649d61002fddfb156ba73ecb4a8ee1e449dc356 100644 (file)
           <context context-type="linenumber">24</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="2710430925353472741" datatype="html">
+        <source>Try to include archive version in merge for non-PDF files</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context>
+          <context context-type="linenumber">32</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5612366187076076264" datatype="html">
         <source>Delete original documents after successful merge</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context>
-          <context context-type="linenumber">32</context>
+          <context context-type="linenumber">36</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5138283234724909648" datatype="html">
         <source>Note that only PDFs will be included.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context>
-          <context context-type="linenumber">34</context>
+          <context context-type="linenumber">39</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8157388568390631653" datatype="html">
         <source>Merged document will be queued for consumption.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">863</context>
+          <context context-type="linenumber">866</context>
         </context-group>
       </trans-unit>
       <trans-unit id="476913782630693351" datatype="html">
         <source>Custom fields updated.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">885</context>
+          <context context-type="linenumber">888</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3873496751167944011" datatype="html">
         <source>Error updating custom fields.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">894</context>
+          <context context-type="linenumber">897</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6307402210351946694" datatype="html">
index 576861ff2c6470f26c4a72db82225a6ff6e1320e..da57feca713b306900c5eccf719b01006b7fd613 100644 (file)
         </select>
     </div>
     <div class="form-check form-switch mt-4">
+      <input class="form-check-input" type="checkbox" role="switch" id="archiveFallbackSwitch" [(ngModel)]="archiveFallback">
+      <label class="form-check-label" for="archiveFallbackSwitch" i18n>Try to include archive version in merge for non-PDF files</label>
+    </div>
+    <div class="form-check form-switch mt-2">
       <input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalsSwitch" [(ngModel)]="deleteOriginals" [disabled]="!userOwnsAllDocuments">
       <label class="form-check-label" for="deleteOriginalsSwitch" i18n>Delete original documents after successful merge</label>
     </div>
-    <p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
+    @if (!archiveFallback) {
+      <p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
+    }
 </div>
 <div class="modal-footer">
     <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
index 9359e40999c755a88d3855af1c550f6a4aa44db4..0438a33e7df72eb95864f7093019ddef8f23087a 100644 (file)
@@ -29,6 +29,7 @@ export class MergeConfirmDialogComponent
   implements OnInit
 {
   public documentIDs: number[] = []
+  public archiveFallback: boolean = false
   public deleteOriginals: boolean = false
   private _documents: Document[] = []
   get documents(): Document[] {
index aa4a07d1283256c3af81ed08064021e35619058f..8f89e541a30d32b392efadc1c902cd6648c92983 100644 (file)
@@ -1040,6 +1040,27 @@ describe('BulkEditorComponent', () => {
       `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
     ) // listAllFilteredIds
     expect(documentListViewService.selected.size).toEqual(0)
+
+    // Test with archiveFallback enabled
+    modal.componentInstance.deleteOriginals = false
+    modal.componentInstance.archiveFallback = true
+    modal.componentInstance.confirm()
+    req = httpTestingController.expectOne(
+      `${environment.apiBaseUrl}documents/bulk_edit/`
+    )
+    req.flush(true)
+    expect(req.request.body).toEqual({
+      documents: [3, 4],
+      method: 'merge',
+      parameters: { metadata_document_id: 3, archive_fallback: true },
+    })
+    httpTestingController.match(
+      `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+    ) // list reload
+    httpTestingController.match(
+      `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+    ) // listAllFilteredIds
+    expect(documentListViewService.selected.size).toEqual(0)
   })
 
   it('should support bulk download with archive, originals or both and file formatting', () => {
index 9864761fabf3bde73bb49603269692ff422b0384..aa32497f31d3d23db37041840c9f3833946f855c 100644 (file)
@@ -857,6 +857,9 @@ export class BulkEditorComponent
         if (mergeDialog.deleteOriginals) {
           args['delete_originals'] = true
         }
+        if (mergeDialog.archiveFallback) {
+          args['archive_fallback'] = true
+        }
         mergeDialog.buttonsEnabled = false
         this.executeBulkOperation(modal, 'merge', args, mergeDialog.documentIDs)
         this.toastService.showInfo(
index be4608e36d13cdb1cca5782e37df0960072abd9d..b8e76f7c800910770587aee215a04a186d71c878 100644 (file)
@@ -318,6 +318,7 @@ def merge(
     *,
     metadata_document_id: int | None = None,
     delete_originals: bool = False,
+    archive_fallback: bool = False,
     user: User | None = None,
 ) -> Literal["OK"]:
     logger.info(
@@ -333,7 +334,14 @@ def merge(
     for doc_id in doc_ids:
         doc = qs.get(id=doc_id)
         try:
-            with pikepdf.open(str(doc.source_path)) as pdf:
+            doc_path = (
+                doc.archive_path
+                if archive_fallback
+                and doc.mime_type != "application/pdf"
+                and doc.has_archive_version
+                else doc.source_path
+            )
+            with pikepdf.open(str(doc_path)) as pdf:
                 version = max(version, pdf.pdf_version)
                 merged_pdf.pages.extend(pdf.pages)
             affected_docs.append(doc.id)
@@ -349,7 +357,7 @@ def merge(
         Path(
             tempfile.mkdtemp(dir=settings.SCRATCH_DIR),
         )
-        / f"{'_'.join([str(doc_id) for doc_id in doc_ids])[:100]}_merged.pdf"
+        / f"{'_'.join([str(doc_id) for doc_id in affected_docs])[:100]}_merged.pdf"
     )
     merged_pdf.remove_unreferenced_resources()
     merged_pdf.save(filepath, min_version=version)
index 38053baeec29114c909e54771aecd050c61282da..a67673efc076bd63c54fb57aeac8830b269d1c3e 100644 (file)
@@ -1446,6 +1446,11 @@ class BulkEditSerializer(
                 raise serializers.ValidationError("delete_originals must be a boolean")
         else:
             parameters["delete_originals"] = False
+        if "archive_fallback" in parameters:
+            if not isinstance(parameters["archive_fallback"], bool):
+                raise serializers.ValidationError("archive_fallback must be a boolean")
+        else:
+            parameters["archive_fallback"] = False
 
     def validate(self, attrs):
         method = attrs["method"]
index 4a7145d34e2fb313b502e8c72358d41d038ccee1..dd59a6217d4c41b4a0e051621eb3a6274e737424 100644 (file)
@@ -514,12 +514,23 @@ class TestPDFActions(DirectoriesMixin, TestCase):
             Path(__file__).parent / "samples" / "simple.jpg",
             img_doc,
         )
+        img_doc_archive = self.dirs.archive_dir / "sample_image.pdf"
+        shutil.copy(
+            Path(__file__).parent
+            / "samples"
+            / "documents"
+            / "originals"
+            / "0000001.pdf",
+            img_doc_archive,
+        )
         self.img_doc = Document.objects.create(
             checksum="D",
             title="D",
             filename=img_doc,
             mime_type="image/jpeg",
         )
+        self.img_doc.archive_filename = img_doc_archive
+        self.img_doc.save()
 
     @mock.patch("documents.tasks.consume_file.s")
     def test_merge(self, mock_consume_file):
@@ -605,6 +616,32 @@ class TestPDFActions(DirectoriesMixin, TestCase):
             doc_ids,
         )
 
+    @mock.patch("documents.tasks.consume_file.s")
+    def test_merge_with_archive_fallback(self, mock_consume_file):
+        """
+        GIVEN:
+            - Existing documents
+        WHEN:
+            - Merge action is called with 2 documents, one of which is an image and archive_fallback is set to True
+        THEN:
+            - Image document should be included
+        """
+        doc_ids = [self.doc2.id, self.img_doc.id]
+
+        result = bulk_edit.merge(doc_ids, archive_fallback=True)
+        self.assertEqual(result, "OK")
+
+        expected_filename = (
+            f"{'_'.join([str(doc_id) for doc_id in doc_ids])[:100]}_merged.pdf"
+        )
+
+        mock_consume_file.assert_called()
+        consume_file_args, _ = mock_consume_file.call_args
+        self.assertEqual(
+            Path(consume_file_args[0].original_file).name,
+            expected_filename,
+        )
+
     @mock.patch("documents.tasks.consume_file.delay")
     @mock.patch("pikepdf.open")
     def test_merge_with_errors(self, mock_open_pdf, mock_consume_file):