mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-20 00:39:32 -06:00
head --> root to avoid confusion, prevent root deletion
[ci skip]
This commit is contained in:
@@ -87,16 +87,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@if (selectedVersionId === version.id) { <span class="ms-2">✓</span> }
|
@if (selectedVersionId === version.id) { <span class="ms-2">✓</span> }
|
||||||
<pngx-confirm-button
|
@if (!version.is_root) {
|
||||||
buttonClasses="btn-link btn-sm text-danger ms-2"
|
<pngx-confirm-button
|
||||||
iconName="trash"
|
buttonClasses="btn-link btn-sm text-danger ms-2"
|
||||||
confirmMessage="Delete this version?"
|
iconName="trash"
|
||||||
i18n-confirmMessage
|
confirmMessage="Delete this version?"
|
||||||
[disabled]="!userIsOwner || !userCanEdit"
|
i18n-confirmMessage
|
||||||
(confirm)="deleteVersion(version.id)"
|
[disabled]="!userIsOwner || !userCanEdit"
|
||||||
>
|
(confirm)="deleteVersion(version.id)"
|
||||||
<span class="visually-hidden" i18n>Delete version</span>
|
>
|
||||||
</pngx-confirm-button>
|
<span class="visually-hidden" i18n>Delete version</span>
|
||||||
|
</pngx-confirm-button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadDocument(documentId: number): void {
|
private loadDocument(documentId: number): void {
|
||||||
let redirectedToHead = false
|
let redirectedToRoot = false
|
||||||
this.selectedVersionId = documentId
|
this.selectedVersionId = documentId
|
||||||
this.previewUrl = this.documentsService.getPreviewUrl(
|
this.previewUrl = this.documentsService.getPreviewUrl(
|
||||||
this.selectedVersionId
|
this.selectedVersionId
|
||||||
@@ -477,14 +477,14 @@ export class DocumentDetailComponent
|
|||||||
.pipe(
|
.pipe(
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
if (error?.status === 404) {
|
if (error?.status === 404) {
|
||||||
return this.documentsService.getHeadId(documentId).pipe(
|
return this.documentsService.getRootId(documentId).pipe(
|
||||||
map((result) => {
|
map((result) => {
|
||||||
const headId = result?.head_id
|
const rootId = result?.root_id
|
||||||
if (headId && headId !== documentId) {
|
if (rootId && rootId !== documentId) {
|
||||||
const section =
|
const section =
|
||||||
this.route.snapshot.paramMap.get('section') || 'details'
|
this.route.snapshot.paramMap.get('section') || 'details'
|
||||||
redirectedToHead = true
|
redirectedToRoot = true
|
||||||
this.router.navigate(['documents', headId, section], {
|
this.router.navigate(['documents', rootId, section], {
|
||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -503,7 +503,7 @@ export class DocumentDetailComponent
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (doc) => {
|
next: (doc) => {
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
if (redirectedToHead) {
|
if (redirectedToRoot) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.router.navigate(['404'], { replaceUrl: true })
|
this.router.navigate(['404'], { replaceUrl: true })
|
||||||
@@ -786,8 +786,6 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVersionBadge(version: DocumentVersionInfo): string {
|
getVersionBadge(version: DocumentVersionInfo): string {
|
||||||
console.log(version)
|
|
||||||
|
|
||||||
const checksum = version?.checksum ?? ''
|
const checksum = version?.checksum ?? ''
|
||||||
if (!checksum) return '----'
|
if (!checksum) return '----'
|
||||||
return checksum.slice(0, 4).toUpperCase()
|
return checksum.slice(0, 4).toUpperCase()
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export interface Document extends ObjectWithPermissions {
|
|||||||
duplicate_documents?: Document[]
|
duplicate_documents?: Document[]
|
||||||
|
|
||||||
// Versioning
|
// Versioning
|
||||||
head_version?: number
|
root_document?: number
|
||||||
versions?: DocumentVersionInfo[]
|
versions?: DocumentVersionInfo[]
|
||||||
|
|
||||||
// Frontend only
|
// Frontend only
|
||||||
@@ -174,4 +174,5 @@ export interface DocumentVersionInfo {
|
|||||||
added?: Date
|
added?: Date
|
||||||
label?: string
|
label?: string
|
||||||
checksum?: string
|
checksum?: string
|
||||||
|
is_root: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,15 +211,15 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeadId(documentId: number) {
|
getRootId(documentId: number) {
|
||||||
return this.http.get<{ head_id: number }>(
|
return this.http.get<{ root_id: number }>(
|
||||||
this.getResourceUrl(documentId, 'head')
|
this.getResourceUrl(documentId, 'root')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteVersion(headDocumentId: number, versionId: number) {
|
deleteVersion(rootDocumentId: number, versionId: number) {
|
||||||
return this.http.delete<{ result: string; current_version_id: number }>(
|
return this.http.delete<{ result: string; current_version_id: number }>(
|
||||||
this.getResourceUrl(headDocumentId, `versions/${versionId}`)
|
this.getResourceUrl(rootDocumentId, `versions/${versionId}`)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -309,13 +309,13 @@ def modify_custom_fields(
|
|||||||
@shared_task
|
@shared_task
|
||||||
def delete(doc_ids: list[int]) -> Literal["OK"]:
|
def delete(doc_ids: list[int]) -> Literal["OK"]:
|
||||||
try:
|
try:
|
||||||
head_ids = (
|
root_ids = (
|
||||||
Document.objects.filter(id__in=doc_ids, head_version__isnull=True)
|
Document.objects.filter(id__in=doc_ids, root_document__isnull=True)
|
||||||
.values_list("id", flat=True)
|
.values_list("id", flat=True)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
version_ids = (
|
version_ids = (
|
||||||
Document.objects.filter(head_version_id__in=head_ids)
|
Document.objects.filter(root_document_id__in=root_ids)
|
||||||
.values_list("id", flat=True)
|
.values_list("id", flat=True)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
@@ -407,7 +407,7 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]:
|
|||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
original_file=filepath,
|
original_file=filepath,
|
||||||
head_version_id=doc.id,
|
root_document_id=doc.id,
|
||||||
),
|
),
|
||||||
overrides,
|
overrides,
|
||||||
)
|
)
|
||||||
@@ -627,7 +627,7 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
|
|||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
original_file=filepath,
|
original_file=filepath,
|
||||||
head_version_id=doc.id,
|
root_document_id=doc.id,
|
||||||
),
|
),
|
||||||
overrides,
|
overrides,
|
||||||
)
|
)
|
||||||
@@ -704,7 +704,7 @@ def edit_pdf(
|
|||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
original_file=filepath,
|
original_file=filepath,
|
||||||
head_version_id=doc.id,
|
root_document_id=doc.id,
|
||||||
),
|
),
|
||||||
overrides,
|
overrides,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ def _resolve_effective_doc(pk: int, request) -> Document | None:
|
|||||||
"""
|
"""
|
||||||
Resolve which Document row should be considered for caching keys:
|
Resolve which Document row should be considered for caching keys:
|
||||||
- If a version is requested, use that version
|
- If a version is requested, use that version
|
||||||
- If pk is a head doc, use its newest child version if present, else the head.
|
- If pk is a root doc, use its newest child version if present, else the root.
|
||||||
- Else, pk is a version, use that version.
|
- Else, pk is a version, use that version.
|
||||||
Returns None if resolution fails (treat as no-cache).
|
Returns None if resolution fails (treat as no-cache).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
request_doc = Document.objects.only("id", "head_version_id").get(pk=pk)
|
request_doc = Document.objects.only("id", "root_document_id").get(pk=pk)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
head_doc = (
|
root_doc = (
|
||||||
request_doc
|
request_doc
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else Document.objects.only("id").get(id=request_doc.head_version_id)
|
else Document.objects.only("id").get(id=request_doc.root_document_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
version_param = (
|
version_param = (
|
||||||
@@ -41,19 +41,22 @@ def _resolve_effective_doc(pk: int, request) -> Document | None:
|
|||||||
if version_param:
|
if version_param:
|
||||||
try:
|
try:
|
||||||
version_id = int(version_param)
|
version_id = int(version_param)
|
||||||
candidate = Document.objects.only("id", "head_version_id").get(
|
candidate = Document.objects.only("id", "root_document_id").get(
|
||||||
id=version_id,
|
id=version_id,
|
||||||
)
|
)
|
||||||
if candidate.id != head_doc.id and candidate.head_version_id != head_doc.id:
|
if (
|
||||||
|
candidate.id != root_doc.id
|
||||||
|
and candidate.root_document_id != root_doc.id
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
return candidate
|
return candidate
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Default behavior: if pk is a head doc, prefer its newest child version
|
# Default behavior: if pk is a root doc, prefer its newest child version
|
||||||
if request_doc.head_version_id is None:
|
if request_doc.root_document_id is None:
|
||||||
latest = head_doc.versions.only("id").order_by("id").last()
|
latest = root_doc.versions.only("id").order_by("id").last()
|
||||||
return latest or head_doc
|
return latest or root_doc
|
||||||
|
|
||||||
# pk is already a version
|
# pk is already a version
|
||||||
return request_doc
|
return request_doc
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ class ConsumerPluginMixin:
|
|||||||
|
|
||||||
self.filename = self.metadata.filename or self.input_doc.original_file.name
|
self.filename = self.metadata.filename or self.input_doc.original_file.name
|
||||||
|
|
||||||
if input_doc.head_version_id:
|
if input_doc.root_document_id:
|
||||||
self.log.debug(f"Document head version id: {input_doc.head_version_id}")
|
self.log.debug(
|
||||||
head_version = Document.objects.get(pk=input_doc.head_version_id)
|
f"Document root document id: {input_doc.root_document_id}",
|
||||||
version_index = head_version.versions.count()
|
)
|
||||||
|
root_document = Document.objects.get(pk=input_doc.root_document_id)
|
||||||
|
version_index = root_document.versions.count()
|
||||||
self.filename += f"_v{version_index}"
|
self.filename += f"_v{version_index}"
|
||||||
|
|
||||||
def _send_progress(
|
def _send_progress(
|
||||||
@@ -483,17 +485,17 @@ class ConsumerPlugin(
|
|||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# store the document.
|
# store the document.
|
||||||
if self.input_doc.head_version_id:
|
if self.input_doc.root_document_id:
|
||||||
# If this is a new version of an existing document, we need
|
# If this is a new version of an existing document, we need
|
||||||
# to make sure we're not creating a new document, but updating
|
# to make sure we're not creating a new document, but updating
|
||||||
# the existing one.
|
# the existing one.
|
||||||
original_document = Document.objects.get(
|
original_document = Document.objects.get(
|
||||||
pk=self.input_doc.head_version_id,
|
pk=self.input_doc.root_document_id,
|
||||||
)
|
)
|
||||||
self.log.debug("Saving record for updated version to database")
|
self.log.debug("Saving record for updated version to database")
|
||||||
original_document.pk = None
|
original_document.pk = None
|
||||||
original_document.head_version = Document.objects.get(
|
original_document.root_document = Document.objects.get(
|
||||||
pk=self.input_doc.head_version_id,
|
pk=self.input_doc.root_document_id,
|
||||||
)
|
)
|
||||||
file_for_checksum = (
|
file_for_checksum = (
|
||||||
self.unmodified_original
|
self.unmodified_original
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class ConsumableDocument:
|
|||||||
|
|
||||||
source: DocumentSource
|
source: DocumentSource
|
||||||
original_file: Path
|
original_file: Path
|
||||||
head_version_id: int | None = None
|
root_document_id: int | None = None
|
||||||
original_path: Path | None = None
|
original_path: Path | None = None
|
||||||
mailrule_id: int | None = None
|
mailrule_id: int | None = None
|
||||||
mime_type: str = dataclasses.field(init=False, default=None)
|
mime_type: str = dataclasses.field(init=False, default=None)
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ class Migration(migrations.Migration):
|
|||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="document",
|
model_name="document",
|
||||||
name="head_version",
|
name="root_document",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="versions",
|
related_name="versions",
|
||||||
to="documents.document",
|
to="documents.document",
|
||||||
verbose_name="head version of document",
|
verbose_name="root document for this version",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "0012_document_head_version"),
|
("documents", "0012_document_root_document"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|||||||
@@ -308,13 +308,13 @@ class Document(SoftDeleteModel, ModelWithOwner):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
head_version = models.ForeignKey(
|
root_document = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="versions",
|
related_name="versions",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_("head version of document"),
|
verbose_name=_("root document for this version"),
|
||||||
)
|
)
|
||||||
|
|
||||||
version_label = models.CharField(
|
version_label = models.CharField(
|
||||||
@@ -441,9 +441,9 @@ class Document(SoftDeleteModel, ModelWithOwner):
|
|||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# If deleting a head document, move all versions to trash as well.
|
# If deleting a root document, move all its versions to trash as well.
|
||||||
if self.head_version_id is None:
|
if self.root_document_id is None:
|
||||||
Document.objects.filter(head_version=self).delete()
|
Document.objects.filter(root_document=self).delete()
|
||||||
return super().delete(
|
return super().delete(
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|||||||
@@ -1046,7 +1046,7 @@ def _get_viewable_duplicates(
|
|||||||
duplicates = Document.global_objects.filter(
|
duplicates = Document.global_objects.filter(
|
||||||
Q(checksum__in=checksums) | Q(archive_checksum__in=checksums),
|
Q(checksum__in=checksums) | Q(archive_checksum__in=checksums),
|
||||||
).exclude(pk=document.pk)
|
).exclude(pk=document.pk)
|
||||||
duplicates = duplicates.filter(head_version__isnull=True)
|
duplicates = duplicates.filter(root_document__isnull=True)
|
||||||
duplicates = duplicates.order_by("-created")
|
duplicates = duplicates.order_by("-created")
|
||||||
allowed = get_objects_for_user_owner_aware(
|
allowed = get_objects_for_user_owner_aware(
|
||||||
user,
|
user,
|
||||||
@@ -1068,6 +1068,7 @@ class DocumentVersionInfoSerializer(serializers.Serializer):
|
|||||||
added = serializers.DateTimeField()
|
added = serializers.DateTimeField()
|
||||||
label = serializers.CharField(required=False, allow_null=True)
|
label = serializers.CharField(required=False, allow_null=True)
|
||||||
checksum = serializers.CharField(required=False, allow_null=True)
|
checksum = serializers.CharField(required=False, allow_null=True)
|
||||||
|
is_root = serializers.BooleanField()
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_serializer(
|
@extend_schema_serializer(
|
||||||
@@ -1090,7 +1091,7 @@ class DocumentSerializer(
|
|||||||
duplicate_documents = SerializerMethodField()
|
duplicate_documents = SerializerMethodField()
|
||||||
|
|
||||||
notes = NotesSerializer(many=True, required=False, read_only=True)
|
notes = NotesSerializer(many=True, required=False, read_only=True)
|
||||||
head_version = serializers.PrimaryKeyRelatedField(read_only=True)
|
root_document = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
versions = SerializerMethodField()
|
versions = SerializerMethodField()
|
||||||
|
|
||||||
custom_fields = CustomFieldInstanceSerializer(
|
custom_fields = CustomFieldInstanceSerializer(
|
||||||
@@ -1127,14 +1128,14 @@ class DocumentSerializer(
|
|||||||
|
|
||||||
@extend_schema_field(DocumentVersionInfoSerializer(many=True))
|
@extend_schema_field(DocumentVersionInfoSerializer(many=True))
|
||||||
def get_versions(self, obj):
|
def get_versions(self, obj):
|
||||||
head_doc = obj if obj.head_version_id is None else obj.head_version
|
root_doc = obj if obj.root_document_id is None else obj.root_document
|
||||||
versions_qs = Document.objects.filter(head_version=head_doc).only(
|
versions_qs = Document.objects.filter(root_document=root_doc).only(
|
||||||
"id",
|
"id",
|
||||||
"added",
|
"added",
|
||||||
"checksum",
|
"checksum",
|
||||||
"version_label",
|
"version_label",
|
||||||
)
|
)
|
||||||
versions = [*versions_qs, head_doc]
|
versions = [*versions_qs, root_doc]
|
||||||
|
|
||||||
def build_info(doc: Document) -> dict[str, object]:
|
def build_info(doc: Document) -> dict[str, object]:
|
||||||
return {
|
return {
|
||||||
@@ -1142,6 +1143,7 @@ class DocumentSerializer(
|
|||||||
"added": doc.added,
|
"added": doc.added,
|
||||||
"label": doc.version_label,
|
"label": doc.version_label,
|
||||||
"checksum": doc.checksum,
|
"checksum": doc.checksum,
|
||||||
|
"is_root": doc.id == root_doc.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
info = [build_info(doc) for doc in versions]
|
info = [build_info(doc) for doc in versions]
|
||||||
@@ -1336,7 +1338,7 @@ class DocumentSerializer(
|
|||||||
"remove_inbox_tags",
|
"remove_inbox_tags",
|
||||||
"page_count",
|
"page_count",
|
||||||
"mime_type",
|
"mime_type",
|
||||||
"head_version",
|
"root_document",
|
||||||
"versions",
|
"versions",
|
||||||
)
|
)
|
||||||
list_serializer_class = OwnedObjectListSerializer
|
list_serializer_class = OwnedObjectListSerializer
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def consume_file(
|
|||||||
|
|
||||||
plugins: list[type[ConsumeTaskPlugin]] = (
|
plugins: list[type[ConsumeTaskPlugin]] = (
|
||||||
[ConsumerPreflightPlugin, ConsumerPlugin]
|
[ConsumerPreflightPlugin, ConsumerPlugin]
|
||||||
if input_doc.head_version_id is not None
|
if input_doc.root_document_id is not None
|
||||||
else [
|
else [
|
||||||
ConsumerPreflightPlugin,
|
ConsumerPreflightPlugin,
|
||||||
AsnCheckPlugin,
|
AsnCheckPlugin,
|
||||||
|
|||||||
@@ -940,7 +940,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
doc_ids,
|
doc_ids,
|
||||||
):
|
):
|
||||||
consumable, overrides = call.args
|
consumable, overrides = call.args
|
||||||
self.assertEqual(consumable.head_version_id, expected_id)
|
self.assertEqual(consumable.root_document_id, expected_id)
|
||||||
self.assertIsNotNone(overrides)
|
self.assertIsNotNone(overrides)
|
||||||
self.assertEqual(result, "OK")
|
self.assertEqual(result, "OK")
|
||||||
|
|
||||||
@@ -990,7 +990,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
self.assertIn(expected_str, output_str)
|
self.assertIn(expected_str, output_str)
|
||||||
self.assertEqual(mock_consume_delay.call_count, 1)
|
self.assertEqual(mock_consume_delay.call_count, 1)
|
||||||
consumable, overrides = mock_consume_delay.call_args[0]
|
consumable, overrides = mock_consume_delay.call_args[0]
|
||||||
self.assertEqual(consumable.head_version_id, self.doc2.id)
|
self.assertEqual(consumable.root_document_id, self.doc2.id)
|
||||||
self.assertIsNotNone(overrides)
|
self.assertIsNotNone(overrides)
|
||||||
self.assertEqual(result, "OK")
|
self.assertEqual(result, "OK")
|
||||||
|
|
||||||
@@ -1013,7 +1013,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
mock_pdf_save.assert_called_once()
|
mock_pdf_save.assert_called_once()
|
||||||
mock_consume_delay.assert_called_once()
|
mock_consume_delay.assert_called_once()
|
||||||
consumable, overrides = mock_consume_delay.call_args[0]
|
consumable, overrides = mock_consume_delay.call_args[0]
|
||||||
self.assertEqual(consumable.head_version_id, self.doc2.id)
|
self.assertEqual(consumable.root_document_id, self.doc2.id)
|
||||||
self.assertTrue(str(consumable.original_file).endswith("_pages_deleted.pdf"))
|
self.assertTrue(str(consumable.original_file).endswith("_pages_deleted.pdf"))
|
||||||
self.assertIsNotNone(overrides)
|
self.assertIsNotNone(overrides)
|
||||||
self.assertEqual(result, "OK")
|
self.assertEqual(result, "OK")
|
||||||
@@ -1162,7 +1162,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
self.assertEqual(result, "OK")
|
self.assertEqual(result, "OK")
|
||||||
mock_consume_delay.assert_called_once()
|
mock_consume_delay.assert_called_once()
|
||||||
consumable, overrides = mock_consume_delay.call_args[0]
|
consumable, overrides = mock_consume_delay.call_args[0]
|
||||||
self.assertEqual(consumable.head_version_id, self.doc2.id)
|
self.assertEqual(consumable.root_document_id, self.doc2.id)
|
||||||
self.assertTrue(str(consumable.original_file).endswith("_edited.pdf"))
|
self.assertTrue(str(consumable.original_file).endswith("_edited.pdf"))
|
||||||
self.assertIsNotNone(overrides)
|
self.assertIsNotNone(overrides)
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class TestDocument(TestCase):
|
|||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
self.assertEqual(mock_unlink.call_count, 2)
|
self.assertEqual(mock_unlink.call_count, 2)
|
||||||
|
|
||||||
def test_delete_head_deletes_versions(self) -> None:
|
def test_delete_root_deletes_versions(self) -> None:
|
||||||
head = Document.objects.create(
|
root = Document.objects.create(
|
||||||
correspondent=Correspondent.objects.create(name="Test0"),
|
correspondent=Correspondent.objects.create(name="Test0"),
|
||||||
title="Head",
|
title="Head",
|
||||||
content="content",
|
content="content",
|
||||||
@@ -87,15 +87,15 @@ class TestDocument(TestCase):
|
|||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
Document.objects.create(
|
Document.objects.create(
|
||||||
head_version=head,
|
root_document=root,
|
||||||
correspondent=head.correspondent,
|
correspondent=root.correspondent,
|
||||||
title="Version",
|
title="Version",
|
||||||
content="content",
|
content="content",
|
||||||
checksum="checksum2",
|
checksum="checksum2",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
head.delete()
|
root.delete()
|
||||||
|
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
self.assertEqual(Document.deleted_objects.count(), 2)
|
self.assertEqual(Document.deleted_objects.count(), 2)
|
||||||
|
|||||||
@@ -778,7 +778,7 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Document.objects.filter(head_version__isnull=True)
|
Document.objects.filter(root_document__isnull=True)
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
.annotate(num_notes=Count("notes"))
|
.annotate(num_notes=Count("notes"))
|
||||||
@@ -805,25 +805,35 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
@action(methods=["get"], detail=True, url_path="head")
|
@action(methods=["get"], detail=True, url_path="root")
|
||||||
def head(self, request, pk=None):
|
def root(self, request, pk=None):
|
||||||
try:
|
try:
|
||||||
doc = Document.global_objects.select_related(
|
doc = Document.global_objects.select_related(
|
||||||
"owner",
|
"owner",
|
||||||
"head_version",
|
"root_document",
|
||||||
).get(pk=pk)
|
).get(pk=pk)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
head_doc = doc if doc.head_version_id is None else doc.head_version
|
root_doc = doc if doc.root_document_id is None else doc.root_document
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
head_doc,
|
root_doc,
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
return Response({"head_id": head_doc.id})
|
return Response({"root_id": root_doc.id})
|
||||||
|
|
||||||
|
@action(methods=["get"], detail=True, url_path="head")
|
||||||
|
def head(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Backwards-compatible alias for the `root` endpoint.
|
||||||
|
"""
|
||||||
|
response = self.root(request, pk=pk)
|
||||||
|
if isinstance(response, Response):
|
||||||
|
return Response({"head_id": response.data.get("root_id")})
|
||||||
|
return response
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
response = super().update(request, *args, **kwargs)
|
response = super().update(request, *args, **kwargs)
|
||||||
@@ -861,7 +871,7 @@ class DocumentViewSet(
|
|||||||
and request.query_params["original"] == "true"
|
and request.query_params["original"] == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _resolve_file_doc(self, head_doc: Document, request):
|
def _resolve_file_doc(self, root_doc: Document, request):
|
||||||
version_param = request.query_params.get("version")
|
version_param = request.query_params.get("version")
|
||||||
if version_param:
|
if version_param:
|
||||||
try:
|
try:
|
||||||
@@ -874,36 +884,39 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
if candidate.id != head_doc.id and candidate.head_version_id != head_doc.id:
|
if (
|
||||||
|
candidate.id != root_doc.id
|
||||||
|
and candidate.root_document_id != root_doc.id
|
||||||
|
):
|
||||||
raise Http404
|
raise Http404
|
||||||
return candidate
|
return candidate
|
||||||
latest = head_doc.versions.order_by("id").last()
|
latest = root_doc.versions.order_by("id").last()
|
||||||
return latest or head_doc
|
return latest or root_doc
|
||||||
|
|
||||||
def file_response(self, pk, request, disposition):
|
def file_response(self, pk, request, disposition):
|
||||||
request_doc = Document.global_objects.select_related("owner").get(id=pk)
|
request_doc = Document.global_objects.select_related("owner").get(id=pk)
|
||||||
head_doc = (
|
root_doc = (
|
||||||
request_doc
|
request_doc
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else Document.global_objects.select_related("owner").get(
|
else Document.global_objects.select_related("owner").get(
|
||||||
id=request_doc.head_version_id,
|
id=request_doc.root_document_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
head_doc,
|
root_doc,
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
# If a version is explicitly requested, use it. Otherwise:
|
# If a version is explicitly requested, use it. Otherwise:
|
||||||
# - if pk is a head document: serve newest version
|
# - if pk is a root document: serve newest version
|
||||||
# - if pk is a version: serve that version
|
# - if pk is a version: serve that version
|
||||||
if "version" in request.query_params:
|
if "version" in request.query_params:
|
||||||
file_doc = self._resolve_file_doc(head_doc, request)
|
file_doc = self._resolve_file_doc(root_doc, request)
|
||||||
else:
|
else:
|
||||||
file_doc = (
|
file_doc = (
|
||||||
self._resolve_file_doc(head_doc, request)
|
self._resolve_file_doc(root_doc, request)
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else request_doc
|
else request_doc
|
||||||
)
|
)
|
||||||
return serve_file(
|
return serve_file(
|
||||||
@@ -945,17 +958,17 @@ class DocumentViewSet(
|
|||||||
def metadata(self, request, pk=None):
|
def metadata(self, request, pk=None):
|
||||||
try:
|
try:
|
||||||
request_doc = Document.objects.select_related("owner").get(pk=pk)
|
request_doc = Document.objects.select_related("owner").get(pk=pk)
|
||||||
head_doc = (
|
root_doc = (
|
||||||
request_doc
|
request_doc
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else Document.objects.select_related("owner").get(
|
else Document.objects.select_related("owner").get(
|
||||||
id=request_doc.head_version_id,
|
id=request_doc.root_document_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
head_doc,
|
root_doc,
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
@@ -963,11 +976,11 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
# Choose the effective document (newest version by default, or explicit via ?version=)
|
# Choose the effective document (newest version by default, or explicit via ?version=)
|
||||||
if "version" in request.query_params:
|
if "version" in request.query_params:
|
||||||
doc = self._resolve_file_doc(head_doc, request)
|
doc = self._resolve_file_doc(root_doc, request)
|
||||||
else:
|
else:
|
||||||
doc = (
|
doc = (
|
||||||
self._resolve_file_doc(head_doc, request)
|
self._resolve_file_doc(root_doc, request)
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else request_doc
|
else request_doc
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1140,26 +1153,26 @@ class DocumentViewSet(
|
|||||||
def preview(self, request, pk=None):
|
def preview(self, request, pk=None):
|
||||||
try:
|
try:
|
||||||
request_doc = Document.objects.select_related("owner").get(id=pk)
|
request_doc = Document.objects.select_related("owner").get(id=pk)
|
||||||
head_doc = (
|
root_doc = (
|
||||||
request_doc
|
request_doc
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else Document.objects.select_related("owner").get(
|
else Document.objects.select_related("owner").get(
|
||||||
id=request_doc.head_version_id,
|
id=request_doc.root_document_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
head_doc,
|
root_doc,
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
if "version" in request.query_params:
|
if "version" in request.query_params:
|
||||||
file_doc = self._resolve_file_doc(head_doc, request)
|
file_doc = self._resolve_file_doc(root_doc, request)
|
||||||
else:
|
else:
|
||||||
file_doc = (
|
file_doc = (
|
||||||
self._resolve_file_doc(head_doc, request)
|
self._resolve_file_doc(root_doc, request)
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else request_doc
|
else request_doc
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1178,25 +1191,25 @@ class DocumentViewSet(
|
|||||||
def thumb(self, request, pk=None):
|
def thumb(self, request, pk=None):
|
||||||
try:
|
try:
|
||||||
request_doc = Document.objects.select_related("owner").get(id=pk)
|
request_doc = Document.objects.select_related("owner").get(id=pk)
|
||||||
head_doc = (
|
root_doc = (
|
||||||
request_doc
|
request_doc
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else Document.objects.select_related("owner").get(
|
else Document.objects.select_related("owner").get(
|
||||||
id=request_doc.head_version_id,
|
id=request_doc.root_document_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
head_doc,
|
root_doc,
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
if "version" in request.query_params:
|
if "version" in request.query_params:
|
||||||
file_doc = self._resolve_file_doc(head_doc, request)
|
file_doc = self._resolve_file_doc(root_doc, request)
|
||||||
else:
|
else:
|
||||||
file_doc = (
|
file_doc = (
|
||||||
self._resolve_file_doc(head_doc, request)
|
self._resolve_file_doc(root_doc, request)
|
||||||
if request_doc.head_version_id is None
|
if request_doc.root_document_id is None
|
||||||
else request_doc
|
else request_doc
|
||||||
)
|
)
|
||||||
handle = file_doc.thumbnail_file
|
handle = file_doc.thumbnail_file
|
||||||
@@ -1526,7 +1539,7 @@ class DocumentViewSet(
|
|||||||
input_doc = ConsumableDocument(
|
input_doc = ConsumableDocument(
|
||||||
source=DocumentSource.ApiUpload,
|
source=DocumentSource.ApiUpload,
|
||||||
original_file=temp_file_path,
|
original_file=temp_file_path,
|
||||||
head_version_id=doc.pk,
|
root_document_id=doc.pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
@@ -1554,10 +1567,10 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
def delete_version(self, request, pk=None, version_id=None):
|
def delete_version(self, request, pk=None, version_id=None):
|
||||||
try:
|
try:
|
||||||
head_doc = Document.objects.select_related("owner").get(pk=pk)
|
root_doc = Document.objects.select_related("owner").get(pk=pk)
|
||||||
if head_doc.head_version_id is not None:
|
if root_doc.root_document_id is not None:
|
||||||
head_doc = Document.objects.select_related("owner").get(
|
root_doc = Document.objects.select_related("owner").get(
|
||||||
pk=head_doc.head_version_id,
|
pk=root_doc.root_document_id,
|
||||||
)
|
)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
@@ -1565,7 +1578,7 @@ class DocumentViewSet(
|
|||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"delete_document",
|
"delete_document",
|
||||||
head_doc,
|
root_doc,
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
@@ -1576,7 +1589,12 @@ class DocumentViewSet(
|
|||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
if version_doc.head_version_id != head_doc.id:
|
if version_doc.id == root_doc.id:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Cannot delete the root/original version. Delete the document instead.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if version_doc.root_document_id != root_doc.id:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
from documents import index
|
from documents import index
|
||||||
@@ -1585,14 +1603,14 @@ class DocumentViewSet(
|
|||||||
version_doc.delete()
|
version_doc.delete()
|
||||||
|
|
||||||
current = (
|
current = (
|
||||||
Document.objects.filter(Q(id=head_doc.id) | Q(head_version=head_doc))
|
Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc))
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"result": "OK",
|
"result": "OK",
|
||||||
"current_version_id": current.id if current else head_doc.id,
|
"current_version_id": current.id if current else root_doc.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user