mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-05 06:19:25 +00:00
A full save() of a Document instance loaded before another process moved its file wrote the old filename back. The DB then pointed at a path that no longer existed, and recovery only worked when the recomputed path happened to match the file's real location. Document.save() now leaves filename and archive_filename out of full saves on existing rows. Writers that own those columns opt in with write_file_paths=True or an explicit update_fields. A full save of a row deleted elsewhere now raises instead of re-inserting it.
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
import pytest
|
|
from django.db import DatabaseError
|
|
from django.db import models
|
|
from django.db import transaction
|
|
|
|
from documents.models import Correspondent
|
|
from documents.models import Document
|
|
from paperless_testing.factories import CorrespondentFactory
|
|
from paperless_testing.factories import DocumentFactory
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestDocument:
|
|
def test_correspondent_deletion_does_not_cascade(self) -> None:
|
|
assert Correspondent.objects.count() == 0
|
|
correspondent = CorrespondentFactory.create()
|
|
assert Correspondent.objects.count() == 1
|
|
|
|
assert Document.objects.count() == 0
|
|
DocumentFactory.create(correspondent=correspondent)
|
|
assert Document.objects.count() == 1
|
|
assert Document.objects.first().correspondent is not None
|
|
|
|
correspondent.delete()
|
|
assert Correspondent.objects.count() == 0
|
|
assert Document.objects.count() == 1
|
|
assert Document.objects.first().correspondent is None
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@pytest.mark.usefixtures("paperless_dirs")
|
|
class TestDocumentFilePathWrites:
|
|
def test_full_save_does_not_write_file_paths(self) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A document whose filename columns were changed in the DB after
|
|
an instance was loaded
|
|
WHEN:
|
|
- The stale instance is saved without update_fields
|
|
THEN:
|
|
- The DB filename columns keep the newer values
|
|
- Other fields from the stale instance are written
|
|
"""
|
|
doc = DocumentFactory(filename="old.pdf", archive_filename="old.pdf")
|
|
stale = Document.objects.get(pk=doc.pk)
|
|
Document.objects.filter(pk=doc.pk).update(
|
|
filename="new.pdf",
|
|
archive_filename="new-archive.pdf",
|
|
)
|
|
|
|
stale.title = "changed"
|
|
stale.save()
|
|
|
|
doc.refresh_from_db()
|
|
assert doc.filename == "new.pdf"
|
|
assert doc.archive_filename == "new-archive.pdf"
|
|
assert doc.title == "changed"
|
|
|
|
@pytest.mark.parametrize(
|
|
"save_kwargs",
|
|
[
|
|
pytest.param({"write_file_paths": True}, id="write-file-paths"),
|
|
pytest.param(
|
|
{"update_fields": ["filename", "archive_filename"]},
|
|
id="explicit-update-fields",
|
|
),
|
|
],
|
|
)
|
|
def test_opt_in_writes_file_paths(self, save_kwargs: dict[str, object]) -> None:
|
|
"""
|
|
GIVEN:
|
|
- An existing document
|
|
WHEN:
|
|
- Its filename columns are changed in memory and saved with an
|
|
explicit opt in
|
|
THEN:
|
|
- The DB filename columns are updated
|
|
"""
|
|
doc = DocumentFactory(filename="old.pdf", archive_filename="old.pdf")
|
|
|
|
doc.filename = "new.pdf"
|
|
doc.archive_filename = "new.pdf"
|
|
doc.save(**save_kwargs)
|
|
|
|
doc.refresh_from_db()
|
|
assert doc.filename == "new.pdf"
|
|
assert doc.archive_filename == "new.pdf"
|
|
|
|
def test_creation_writes_file_paths(self) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A new, unsaved document with filename columns set
|
|
WHEN:
|
|
- It is saved
|
|
THEN:
|
|
- The filename columns are written
|
|
"""
|
|
doc = DocumentFactory.build(filename="a.pdf", archive_filename="b.pdf")
|
|
|
|
doc.save()
|
|
|
|
doc.refresh_from_db()
|
|
assert doc.filename == "a.pdf"
|
|
assert doc.archive_filename == "b.pdf"
|
|
|
|
def test_full_save_of_deleted_row_raises(self) -> None:
|
|
"""
|
|
GIVEN:
|
|
- An instance whose row was hard-deleted elsewhere
|
|
WHEN:
|
|
- The instance is saved without update_fields
|
|
THEN:
|
|
- A DatabaseError is raised instead of re-inserting the row
|
|
"""
|
|
doc = DocumentFactory()
|
|
stale = Document.objects.get(pk=doc.pk)
|
|
# A plain QuerySet bypasses the soft-delete manager, so the row is
|
|
# really gone (as after emptying the trash).
|
|
models.QuerySet(model=Document).filter(pk=doc.pk).delete()
|
|
|
|
# The savepoint keeps the failed UPDATE from poisoning the test transaction.
|
|
with pytest.raises(DatabaseError), transaction.atomic():
|
|
stale.save()
|
|
|
|
assert not Document.global_objects.filter(pk=doc.pk).exists()
|