mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-08 21:23:44 -05:00
Compare commits
4 Commits
9581cfa1e3
...
2337fc155c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2337fc155c | ||
![]() |
a5ab90de34 | ||
![]() |
3d92f202b0 | ||
![]() |
18d9f5c2da |
@@ -59,7 +59,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
||||||
<i-bs name="pencil"></i-bs> <ng-container i18n>Edit PDF</ng-container>
|
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1356,7 +1356,7 @@ export class DocumentDetailComponent
|
|||||||
size: 'xl',
|
size: 'xl',
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
})
|
})
|
||||||
modal.componentInstance.title = $localize`Edit PDF`
|
modal.componentInstance.title = $localize`PDF Editor`
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.documentID = this.document.id
|
modal.componentInstance.documentID = this.document.id
|
||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
|
@@ -532,7 +532,7 @@ def edit_pdf(
|
|||||||
logger.error(
|
logger.error(
|
||||||
"Update requested but multiple output documents specified",
|
"Update requested but multiple output documents specified",
|
||||||
)
|
)
|
||||||
return "ERROR"
|
raise ValueError("Multiple output documents specified")
|
||||||
|
|
||||||
for op in operations:
|
for op in operations:
|
||||||
dst = pdf_docs[op.get("doc", 0)]
|
dst = pdf_docs[op.get("doc", 0)]
|
||||||
@@ -542,9 +542,13 @@ def edit_pdf(
|
|||||||
dst.pages[-1].rotate(op["rotate"], relative=True)
|
dst.pages[-1].rotate(op["rotate"], relative=True)
|
||||||
|
|
||||||
if update_document:
|
if update_document:
|
||||||
|
temp_path = doc.source_path.with_suffix(".tmp.pdf")
|
||||||
pdf = pdf_docs[0]
|
pdf = pdf_docs[0]
|
||||||
pdf.remove_unreferenced_resources()
|
pdf.remove_unreferenced_resources()
|
||||||
pdf.save(doc.source_path)
|
# save the edited PDF to a temporary file in case of errors
|
||||||
|
pdf.save(temp_path)
|
||||||
|
# replace the original document with the edited one
|
||||||
|
temp_path.replace(doc.source_path)
|
||||||
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
||||||
doc.page_count = len(pdf.pages)
|
doc.page_count = len(pdf.pages)
|
||||||
doc.save()
|
doc.save()
|
||||||
@@ -583,7 +587,9 @@ 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"
|
raise ValueError(
|
||||||
|
f"An error occurred while editing the document: {e}",
|
||||||
|
) from e
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
@@ -1524,7 +1524,7 @@ class BulkEditSerializer(
|
|||||||
else:
|
else:
|
||||||
parameters["archive_fallback"] = False
|
parameters["archive_fallback"] = False
|
||||||
|
|
||||||
def _validate_parameters_edit_pdf(self, parameters):
|
def _validate_parameters_edit_pdf(self, parameters, document_id):
|
||||||
if "operations" not in parameters:
|
if "operations" not in parameters:
|
||||||
raise serializers.ValidationError("operations not specified")
|
raise serializers.ValidationError("operations not specified")
|
||||||
if not isinstance(parameters["operations"], list):
|
if not isinstance(parameters["operations"], list):
|
||||||
@@ -1556,6 +1556,15 @@ class BulkEditSerializer(
|
|||||||
"update_document only allowed with a single output document",
|
"update_document only allowed with a single output document",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
doc = Document.objects.get(id=document_id)
|
||||||
|
# doc existence is already validated
|
||||||
|
if doc.page_count:
|
||||||
|
for op in parameters["operations"]:
|
||||||
|
if op["page"] < 1 or op["page"] > doc.page_count:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
method = attrs["method"]
|
method = attrs["method"]
|
||||||
parameters = attrs["parameters"]
|
parameters = attrs["parameters"]
|
||||||
@@ -1595,7 +1604,7 @@ class BulkEditSerializer(
|
|||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
"Edit PDF method only supports one document",
|
"Edit PDF method only supports one document",
|
||||||
)
|
)
|
||||||
self._validate_parameters_edit_pdf(parameters)
|
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@@ -41,6 +41,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
title="B",
|
title="B",
|
||||||
correspondent=self.c1,
|
correspondent=self.c1,
|
||||||
document_type=self.dt1,
|
document_type=self.dt1,
|
||||||
|
page_count=5,
|
||||||
)
|
)
|
||||||
self.doc3 = Document.objects.create(
|
self.doc3 = Document.objects.create(
|
||||||
checksum="C",
|
checksum="C",
|
||||||
@@ -1555,6 +1556,32 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
response.content,
|
response.content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
|
||||||
|
def test_edit_pdf_page_out_of_bounds(self, m):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- API data for editing PDF is called
|
||||||
|
- The page number is out of bounds
|
||||||
|
WHEN:
|
||||||
|
- API is called
|
||||||
|
THEN:
|
||||||
|
- The API fails with a correct error code
|
||||||
|
"""
|
||||||
|
self.setup_mock(m, "edit_pdf")
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 99}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"out of bounds", response.content)
|
||||||
|
|
||||||
@override_settings(AUDIT_LOG_ENABLED=True)
|
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||||
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
||||||
"""
|
"""
|
||||||
|
@@ -1032,8 +1032,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
{"page": 9999}, # invalid page, forces error during PDF load
|
{"page": 9999}, # invalid page, forces error during PDF load
|
||||||
]
|
]
|
||||||
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
||||||
result = bulk_edit.edit_pdf(doc_ids, operations)
|
with self.assertRaises(Exception):
|
||||||
self.assertEqual(result, "ERROR")
|
bulk_edit.edit_pdf(doc_ids, operations)
|
||||||
mock_group.assert_not_called()
|
mock_group.assert_not_called()
|
||||||
mock_consume_file.assert_not_called()
|
mock_consume_file.assert_not_called()
|
||||||
|
|
||||||
@@ -1058,7 +1058,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
{"page": 2, "doc": 1},
|
{"page": 2, "doc": 1},
|
||||||
]
|
]
|
||||||
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
||||||
result = bulk_edit.edit_pdf(doc_ids, operations, update_document=True)
|
with self.assertRaises(ValueError):
|
||||||
self.assertEqual(result, "ERROR")
|
bulk_edit.edit_pdf(doc_ids, operations, update_document=True)
|
||||||
mock_group.assert_not_called()
|
mock_group.assert_not_called()
|
||||||
mock_consume_file.assert_not_called()
|
mock_consume_file.assert_not_called()
|
||||||
|
@@ -1427,7 +1427,6 @@ class BulkEditView(PassUserMixin):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: parameter validation
|
|
||||||
result = method(documents, **parameters)
|
result = method(documents, **parameters)
|
||||||
|
|
||||||
if settings.AUDIT_LOG_ENABLED and modified_field:
|
if settings.AUDIT_LOG_ENABLED and modified_field:
|
||||||
|
Reference in New Issue
Block a user