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)
|
- `"merge": true or false` (defaults to false)
|
||||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||||
removing them) or be merged with existing permissions.
|
removing them) or be merged with existing permissions.
|
||||||
- `merge`
|
- `merge` and `merge_and_delete_originals`
|
||||||
- No additional `parameters` required.
|
- No additional `parameters` required.
|
||||||
- The ordering of the merged document is determined by the list of IDs.
|
- The ordering of the merged document is determined by the list of IDs.
|
||||||
- Optional `parameters`:
|
- Optional `parameters`:
|
||||||
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
- `"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`
|
- `split`
|
||||||
- Requires `parameters`:
|
- 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]"`
|
- `"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 context-type="linenumber">24</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</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">
|
<trans-unit id="5138283234724909648" datatype="html">
|
||||||
<source>Note that only PDFs will be included.</source>
|
<source>Note that only PDFs will be included.</source>
|
||||||
<context-group purpose="location">
|
<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="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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8157388568390631653" datatype="html">
|
<trans-unit id="8157388568390631653" datatype="html">
|
||||||
@ -6417,7 +6424,7 @@
|
|||||||
<source>Merged document will be queued for consumption.</source>
|
<source>Merged document will be queued for consumption.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2784168796433474565" datatype="html">
|
<trans-unit id="2784168796433474565" datatype="html">
|
||||||
|
@ -27,6 +27,12 @@
|
|||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
|||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
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 { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
@ -16,6 +17,7 @@ export class MergeConfirmDialogComponent
|
|||||||
implements OnInit
|
implements OnInit
|
||||||
{
|
{
|
||||||
public documentIDs: number[] = []
|
public documentIDs: number[] = []
|
||||||
|
public deleteOriginals: boolean = false
|
||||||
private _documents: Document[] = []
|
private _documents: Document[] = []
|
||||||
get documents(): Document[] {
|
get documents(): Document[] {
|
||||||
return this._documents
|
return this._documents
|
||||||
@ -27,7 +29,8 @@ export class MergeConfirmDialogComponent
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
activeModal: NgbActiveModal,
|
activeModal: NgbActiveModal,
|
||||||
private documentService: DocumentService
|
private documentService: DocumentService,
|
||||||
|
private permissionService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super(activeModal)
|
super(activeModal)
|
||||||
}
|
}
|
||||||
@ -48,4 +51,10 @@ export class MergeConfirmDialogComponent
|
|||||||
getDocument(documentID: number): Document {
|
getDocument(documentID: number): Document {
|
||||||
return this.documents.find((d) => d.id === documentID)
|
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
|
args['metadata_document_id'] = mergeDialog.metadataDocumentID
|
||||||
}
|
}
|
||||||
mergeDialog.buttonsEnabled = false
|
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(
|
this.toastService.showInfo(
|
||||||
$localize`Merged document will be queued for consumption.`
|
$localize`Merged document will be queued for consumption.`
|
||||||
)
|
)
|
||||||
|
@ -234,7 +234,11 @@ def rotate(doc_ids: list[int], degrees: int):
|
|||||||
return "OK"
|
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(
|
logger.info(
|
||||||
f"Attempting to merge {len(doc_ids)} documents into a single document.",
|
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,
|
overrides,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if delete_originals:
|
||||||
|
logger.info("Removing original documents after merge")
|
||||||
|
delete(affected_docs)
|
||||||
|
|
||||||
return "OK"
|
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]]):
|
def split(doc_ids: list[int], pages: list[list[int]]):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Attempting to split document {doc_ids[0]} into {len(pages)} documents",
|
f"Attempting to split document {doc_ids[0]} into {len(pages)} documents",
|
||||||
|
@ -943,6 +943,7 @@ class BulkEditSerializer(
|
|||||||
"set_permissions",
|
"set_permissions",
|
||||||
"rotate",
|
"rotate",
|
||||||
"merge",
|
"merge",
|
||||||
|
"merge_and_delete_originals",
|
||||||
"split",
|
"split",
|
||||||
"delete_pages",
|
"delete_pages",
|
||||||
],
|
],
|
||||||
@ -999,6 +1000,8 @@ class BulkEditSerializer(
|
|||||||
return bulk_edit.rotate
|
return bulk_edit.rotate
|
||||||
elif method == "merge":
|
elif method == "merge":
|
||||||
return bulk_edit.merge
|
return bulk_edit.merge
|
||||||
|
elif method == "merge_and_delete_originals":
|
||||||
|
return bulk_edit.merge_and_delete_originals
|
||||||
elif method == "split":
|
elif method == "split":
|
||||||
return bulk_edit.split
|
return bulk_edit.split
|
||||||
elif method == "delete_pages":
|
elif method == "delete_pages":
|
||||||
|
@ -376,6 +376,45 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
/ "0000003.pdf",
|
/ "0000003.pdf",
|
||||||
sample3,
|
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(
|
self.doc1 = Document.objects.create(
|
||||||
checksum="A",
|
checksum="A",
|
||||||
title="A",
|
title="A",
|
||||||
@ -410,6 +449,24 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
mime_type="image/jpeg",
|
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")
|
@mock.patch("documents.tasks.consume_file.delay")
|
||||||
def test_merge(self, mock_consume_file):
|
def test_merge(self, mock_consume_file):
|
||||||
"""
|
"""
|
||||||
@ -444,6 +501,41 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result, "OK")
|
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("documents.tasks.consume_file.delay")
|
||||||
@mock.patch("pikepdf.open")
|
@mock.patch("pikepdf.open")
|
||||||
def test_merge_with_errors(self, mock_open_pdf, mock_consume_file):
|
def test_merge_with_errors(self, mock_open_pdf, mock_consume_file):
|
||||||
|
@ -968,7 +968,12 @@ class BulkEditView(PassUserMixin):
|
|||||||
has_perms = (
|
has_perms = (
|
||||||
all((doc.owner == user or doc.owner is None) for doc in document_objs)
|
all((doc.owner == user or doc.owner is None) for doc in document_objs)
|
||||||
if method
|
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(
|
else all(
|
||||||
has_perms_owner_aware(user, "change_document", doc)
|
has_perms_owner_aware(user, "change_document", doc)
|
||||||
for doc in document_objs
|
for doc in document_objs
|
||||||
|
Loading…
x
Reference in New Issue
Block a user