From b7d3be6f756427f006b4df2d11dc87fc4bbb8b6e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:27:49 -0800 Subject: [PATCH] Make content edits target a specific version --- .../document-detail.component.spec.ts | 12 ++++++ .../document-detail.component.ts | 4 +- .../services/rest/document.service.spec.ts | 8 ++++ .../src/app/services/rest/document.service.ts | 7 +++- .../tests/test_api_document_versions.py | 37 +++++++++++++++++++ src/documents/views.py | 19 +++++++--- 6 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index f4cf1b6b9..39c55e8da 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -740,6 +740,18 @@ describe('DocumentDetailComponent', () => { ) }) + it('save should target currently selected version', () => { + initNormally() + component.selectedVersionId = 10 + const patchSpy = jest.spyOn(documentService, 'patch') + patchSpy.mockReturnValue(of(doc)) + + component.save() + + expect(patchSpy).toHaveBeenCalled() + expect(patchSpy.mock.calls[0][1]).toEqual(10) + }) + it('should show toast error on save if error occurs', () => { currentUserHasObjectPermissions = true initNormally() diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 523974e6a..06ae6a6c5 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -1079,7 +1079,7 @@ export class DocumentDetailComponent this.networkActive = true ;(document.activeElement as HTMLElement)?.dispatchEvent(new Event('change')) this.documentsService - .patch(this.getChangedFields()) + .patch(this.getChangedFields(), this.selectedVersionId) .pipe(first()) .subscribe({ next: (docValues) => { @@ -1134,7 +1134,7 @@ export class DocumentDetailComponent this.networkActive = true this.store.next(this.documentForm.value) this.documentsService - .patch(this.getChangedFields()) + .patch(this.getChangedFields(), this.selectedVersionId) .pipe( switchMap((updateResult) => { this.savedViewService.maybeRefreshDocumentCounts() diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts index 6ded03e81..a8dff81cc 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -298,6 +298,14 @@ describe(`DocumentService`, () => { expect(req.request.body.remove_inbox_tags).toEqual(true) }) + it('should pass selected version to patch when provided', () => { + subscription = service.patch(documents[0], 123).subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/?version=123` + ) + expect(req.request.method).toEqual('PATCH') + }) + it('should call appropriate api endpoint for getting audit log', () => { subscription = service.getHistory(documents[0].id).subscribe() const req = httpTestingController.expectOne( diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 21dece81a..00e894278 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -227,11 +227,14 @@ export class DocumentService extends AbstractPaperlessService { return this.http.get(this.getResourceUrl(null, 'next_asn')) } - patch(o: Document): Observable { + patch(o: Document, versionID: number = null): Observable { o.remove_inbox_tags = !!this.settingsService.get( SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS ) - return super.patch(o) + this.clearCache() + return this.http.patch(this.getResourceUrl(o.id), o, { + params: versionID ? { version: versionID.toString() } : {}, + }) } uploadDocument(formData) { diff --git a/src/documents/tests/test_api_document_versions.py b/src/documents/tests/test_api_document_versions.py index a6b2b72f1..775de1e0b 100644 --- a/src/documents/tests/test_api_document_versions.py +++ b/src/documents/tests/test_api_document_versions.py @@ -499,6 +499,43 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): self.assertEqual(root.content, "root-content") self.assertEqual(v1.content, "v1-content") + def test_patch_content_updates_selected_version_content(self) -> None: + root = Document.objects.create( + title="root", + checksum="root", + mime_type="application/pdf", + content="root-content", + ) + v1 = Document.objects.create( + title="v1", + checksum="v1", + mime_type="application/pdf", + root_document=root, + content="v1-content", + ) + v2 = Document.objects.create( + title="v2", + checksum="v2", + mime_type="application/pdf", + root_document=root, + content="v2-content", + ) + + resp = self.client.patch( + f"/api/documents/{root.id}/?version={v1.id}", + {"content": "edited-v1"}, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_200_OK) + self.assertEqual(resp.data["content"], "edited-v1") + root.refresh_from_db() + v1.refresh_from_db() + v2.refresh_from_db() + self.assertEqual(v1.content, "edited-v1") + self.assertEqual(v2.content, "v2-content") + self.assertEqual(root.content, "root-content") + def test_retrieve_returns_latest_version_content(self) -> None: root = Document.objects.create( title="root", diff --git a/src/documents/views.py b/src/documents/views.py index 82639843e..fd4aecacd 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -859,13 +859,17 @@ class DocumentViewSet( def update(self, request, *args, **kwargs): partial = kwargs.pop("partial", False) root_doc = self.get_object() + content_doc = ( + self._resolve_file_doc(root_doc, request) + if "version" in request.query_params + else self._get_latest_doc_for_root(root_doc) + ) content_updated = "content" in request.data updated_content = request.data.get("content") if content_updated else None - latest_doc = self._get_latest_doc_for_root(root_doc) data = request.data.copy() serializer_partial = partial - if content_updated and latest_doc.id != root_doc.id: + if content_updated and content_doc.id != root_doc.id: if updated_content is None: raise ValidationError({"content": ["This field may not be null."]}) data.pop("content", None) @@ -879,15 +883,18 @@ class DocumentViewSet( serializer.is_valid(raise_exception=True) self.perform_update(serializer) - if content_updated and latest_doc.id != root_doc.id: - latest_doc.content = updated_content - latest_doc.save(update_fields=["content", "modified"]) + if content_updated and content_doc.id != root_doc.id: + content_doc.content = updated_content + content_doc.save(update_fields=["content", "modified"]) if getattr(root_doc, "_prefetched_objects_cache", None): root_doc._prefetched_objects_cache = {} refreshed_doc = self.get_queryset().get(pk=root_doc.pk) - response = Response(self.get_serializer(refreshed_doc).data) + response_data = self.get_serializer(refreshed_doc).data + if "version" in request.query_params and "content" in response_data: + response_data["content"] = content_doc.content + response = Response(response_data) from documents import index