Add option to delete original documents after merge
This commit is contained in:
parent
d8c96b6e4a
commit
ef9d1b5188
@ -412,11 +412,14 @@ The following methods are supported:
|
||||
- `"merge": true or false` (defaults to false)
|
||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||
removing them) or be merged with existing permissions.
|
||||
- `merge`
|
||||
- `merge` and `merge_and_delete_originals`
|
||||
- No additional `parameters` required.
|
||||
- The ordering of the merged document is determined by the list of IDs.
|
||||
- Optional `parameters`:
|
||||
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
||||
- As the name implies, `merge_and_delete_originals` deletes the original
|
||||
documents after merging. This requires the calling user being the owner of
|
||||
all documents that are merged.
|
||||
- `split`
|
||||
- Requires `parameters`:
|
||||
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
|
||||
|
@ -2970,11 +2970,18 @@
|
||||
<context context-type="linenumber">24</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2519605321077387027" datatype="html">
|
||||
<source> Delete original documents after 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,34</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">30</context>
|
||||
<context context-type="linenumber">36</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8157388568390631653" datatype="html">
|
||||
@ -6417,7 +6424,7 @@
|
||||
<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">819</context>
|
||||
<context context-type="linenumber">824</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2784168796433474565" datatype="html">
|
||||
|
@ -27,6 +27,12 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" value="" id="deleteOriginals" [(ngModel)]="deleteOriginals" [disabled]="!userOwnsAllDocuments">
|
||||
<label class="form-check-label" for="deleteOrginals" i18n>
|
||||
Delete original documents after merge
|
||||
</label>
|
||||
</div>
|
||||
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
@ -16,6 +17,7 @@ export class MergeConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
public documentIDs: number[] = []
|
||||
public deleteOriginals: boolean = false
|
||||
private _documents: Document[] = []
|
||||
get documents(): Document[] {
|
||||
return this._documents
|
||||
@ -27,7 +29,8 @@ export class MergeConfirmDialogComponent
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
private documentService: DocumentService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super(activeModal)
|
||||
}
|
||||
@ -48,4 +51,10 @@ export class MergeConfirmDialogComponent
|
||||
getDocument(documentID: number): Document {
|
||||
return this.documents.find((d) => d.id === documentID)
|
||||
}
|
||||
|
||||
get userOwnsAllDocuments(): boolean {
|
||||
return this.documents.every((d) =>
|
||||
this.permissionService.currentUserOwnsObject(d)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -814,7 +814,12 @@ export class BulkEditorComponent
|
||||
args['metadata_document_id'] = mergeDialog.metadataDocumentID
|
||||
}
|
||||
mergeDialog.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'merge', args, mergeDialog.documentIDs)
|
||||
this.executeBulkOperation(
|
||||
modal,
|
||||
mergeDialog.deleteOriginals ? 'merge_and_delete_originals' : 'merge',
|
||||
args,
|
||||
mergeDialog.documentIDs
|
||||
)
|
||||
this.toastService.showInfo(
|
||||
$localize`Merged document will be queued for consumption.`
|
||||
)
|
||||
|
@ -234,7 +234,11 @@ def rotate(doc_ids: list[int], degrees: int):
|
||||
return "OK"
|
||||
|
||||
|
||||
def merge(doc_ids: list[int], metadata_document_id: Optional[int] = None):
|
||||
def merge(
|
||||
doc_ids: list[int],
|
||||
metadata_document_id: Optional[int] = None,
|
||||
delete_originals: bool = False,
|
||||
):
|
||||
logger.info(
|
||||
f"Attempting to merge {len(doc_ids)} documents into a single document.",
|
||||
)
|
||||
@ -285,9 +289,20 @@ def merge(doc_ids: list[int], metadata_document_id: Optional[int] = None):
|
||||
overrides,
|
||||
)
|
||||
|
||||
if delete_originals:
|
||||
logger.info("Removing original documents after merge")
|
||||
delete(affected_docs)
|
||||
|
||||
return "OK"
|
||||
|
||||
|
||||
def merge_and_delete_originals(
|
||||
doc_ids: list[int],
|
||||
metadata_document_id: Optional[int] = None,
|
||||
):
|
||||
return merge(doc_ids, metadata_document_id, True)
|
||||
|
||||
|
||||
def split(doc_ids: list[int], pages: list[list[int]]):
|
||||
logger.info(
|
||||
f"Attempting to split document {doc_ids[0]} into {len(pages)} documents",
|
||||
|
@ -943,6 +943,7 @@ class BulkEditSerializer(
|
||||
"set_permissions",
|
||||
"rotate",
|
||||
"merge",
|
||||
"merge_and_delete_originals",
|
||||
"split",
|
||||
"delete_pages",
|
||||
],
|
||||
@ -999,6 +1000,8 @@ class BulkEditSerializer(
|
||||
return bulk_edit.rotate
|
||||
elif method == "merge":
|
||||
return bulk_edit.merge
|
||||
elif method == "merge_and_delete_originals":
|
||||
return bulk_edit.merge_and_delete_originals
|
||||
elif method == "split":
|
||||
return bulk_edit.split
|
||||
elif method == "delete_pages":
|
||||
|
@ -376,6 +376,45 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
/ "0000003.pdf",
|
||||
sample3,
|
||||
)
|
||||
|
||||
sample4 = self.dirs.scratch_dir / "sample4.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000001.pdf",
|
||||
sample4,
|
||||
)
|
||||
sample4_archive = self.dirs.archive_dir / "sample4_archive.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000001.pdf",
|
||||
sample4_archive,
|
||||
)
|
||||
|
||||
sample5 = self.dirs.scratch_dir / "sample5.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000002.pdf",
|
||||
sample5,
|
||||
)
|
||||
sample5_archive = self.dirs.archive_dir / "sample5_archive.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000002.pdf",
|
||||
sample5_archive,
|
||||
)
|
||||
|
||||
self.doc1 = Document.objects.create(
|
||||
checksum="A",
|
||||
title="A",
|
||||
@ -410,6 +449,24 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
mime_type="image/jpeg",
|
||||
)
|
||||
|
||||
self.doc1_delete_after_merge = Document.objects.create(
|
||||
checksum="Ad",
|
||||
title="Adelete",
|
||||
filename=sample4,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.doc1_delete_after_merge.archive_filename = sample4_archive
|
||||
self.doc1_delete_after_merge.save()
|
||||
|
||||
self.doc2_delete_after_merge = Document.objects.create(
|
||||
checksum="Bd",
|
||||
title="Bdelete",
|
||||
filename=sample5,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.doc2_delete_after_merge.archive_filename = sample5_archive
|
||||
self.doc2_delete_after_merge.save()
|
||||
|
||||
@mock.patch("documents.tasks.consume_file.delay")
|
||||
def test_merge(self, mock_consume_file):
|
||||
"""
|
||||
@ -444,6 +501,41 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
|
||||
self.assertEqual(result, "OK")
|
||||
|
||||
@mock.patch("documents.tasks.consume_file.delay")
|
||||
def test_merge_and_delete_originals(self, mock_consume_file):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing documents
|
||||
WHEN:
|
||||
- Merge action with deleting documents is called with 2 documents
|
||||
THEN:
|
||||
- Consume file should be called
|
||||
- Documents should be deleted
|
||||
"""
|
||||
doc_ids = [self.doc1_delete_after_merge.id, self.doc2_delete_after_merge.id]
|
||||
|
||||
result = bulk_edit.merge_and_delete_originals(doc_ids)
|
||||
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,
|
||||
)
|
||||
self.assertEqual(consume_file_args[1].title, None)
|
||||
|
||||
with self.assertRaises(Document.DoesNotExist):
|
||||
Document.objects.get(id=self.doc1_delete_after_merge.id)
|
||||
|
||||
with self.assertRaises(Document.DoesNotExist):
|
||||
Document.objects.get(id=self.doc2_delete_after_merge.id)
|
||||
|
||||
@mock.patch("documents.tasks.consume_file.delay")
|
||||
@mock.patch("pikepdf.open")
|
||||
def test_merge_with_errors(self, mock_open_pdf, mock_consume_file):
|
||||
|
@ -968,7 +968,12 @@ class BulkEditView(PassUserMixin):
|
||||
has_perms = (
|
||||
all((doc.owner == user or doc.owner is None) for doc in document_objs)
|
||||
if method
|
||||
in [bulk_edit.set_permissions, bulk_edit.delete, bulk_edit.rotate]
|
||||
in [
|
||||
bulk_edit.set_permissions,
|
||||
bulk_edit.delete,
|
||||
bulk_edit.rotate,
|
||||
bulk_edit.merge_and_delete_originals,
|
||||
]
|
||||
else all(
|
||||
has_perms_owner_aware(user, "change_document", doc)
|
||||
for doc in document_objs
|
||||
|
Loading…
x
Reference in New Issue
Block a user