head --> root to avoid confusion, prevent root deletion

[ci skip]
This commit is contained in:
shamoon
2026-02-10 16:26:13 -08:00
parent 7fa400f486
commit 8014932419
16 changed files with 150 additions and 124 deletions

View File

@@ -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>
} }

View File

@@ -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()

View File

@@ -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
} }

View File

@@ -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}`)
) )
} }

View File

@@ -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,
) )

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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",
), ),
), ),
] ]

View File

@@ -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 = [

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,
}, },
) )