From a9e3ba4f694db1695e6a91e4a24228742ef8f04e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:53:46 -0700 Subject: [PATCH] Fix: ensure remove inbox tag children on remove_inbox_tags --- src/documents/serialisers.py | 24 ++++++++------ src/documents/tests/test_tag_hierarchy.py | 39 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 94504fcd0..af2b4c02e 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1256,20 +1256,26 @@ class DocumentSerializer( if "tags" in validated_data else [] ) - inbox_tags_not_being_added = Tag.objects.filter(is_inbox_tag=True).exclude( - id__in=tag_ids_being_added, - ) + tags_being_added = Tag.objects.filter(id__in=tag_ids_being_added) + required_by_add_tags = set(tags_being_added) + for tag in tags_being_added: + required_by_add_tags.update(tag.get_ancestors()) + + # Remove its descendants too, except any that is being added in this same update + tags_to_remove = set() + for tag in Tag.objects.filter(is_inbox_tag=True): + if tag in required_by_add_tags: + continue + tags_to_remove.add(tag) + tags_to_remove.update(tag.get_descendants()) + if "tags" in validated_data: validated_data["tags"] = [ - tag - for tag in validated_data["tags"] - if tag not in inbox_tags_not_being_added + tag for tag in validated_data["tags"] if tag not in tags_to_remove ] else: validated_data["tags"] = [ - tag - for tag in instance.tags.all() - if tag not in inbox_tags_not_being_added + tag for tag in instance.tags.all() if tag not in tags_to_remove ] if settings.AUDIT_LOG_ENABLED: diff --git a/src/documents/tests/test_tag_hierarchy.py b/src/documents/tests/test_tag_hierarchy.py index b439d5d70..ed0bff351 100644 --- a/src/documents/tests/test_tag_hierarchy.py +++ b/src/documents/tests/test_tag_hierarchy.py @@ -2,6 +2,7 @@ from unittest import mock from django.contrib.auth.models import Permission from django.contrib.auth.models import User +from rest_framework import status from rest_framework.test import APITestCase from documents import bulk_edit @@ -108,6 +109,44 @@ class TestTagHierarchy(DirectoriesMixin, APITestCase): self.document.refresh_from_db() assert self.document.tags.count() == 0 + def test_remove_inbox_tags_removes_nested_children(self) -> None: + inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True) + nested = Tag.objects.create(name="Nested", tn_parent=inbox) + self.document.add_nested_tags([nested]) + + resp = self.client.patch( + f"/api/documents/{self.document.pk}/", + {"title": "new title", "remove_inbox_tags": True}, + format="json", + ) + assert resp.status_code == status.HTTP_200_OK + self.document.refresh_from_db() + assert self.document.tags.count() == 0 + + # A subsequent save must not re-add the inbox tag as an ancestor + resp = self.client.patch( + f"/api/documents/{self.document.pk}/", + {"title": "another title", "tags": [], "remove_inbox_tags": True}, + format="json", + ) + assert resp.status_code == status.HTTP_200_OK + self.document.refresh_from_db() + assert self.document.tags.count() == 0 + + def test_remove_inbox_tags_keeps_inbox_when_nested_child_added(self) -> None: + inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True) + nested = Tag.objects.create(name="Nested", tn_parent=inbox) + self.document.add_nested_tags([inbox]) + + self.client.patch( + f"/api/documents/{self.document.pk}/", + {"tags": [nested.pk], "remove_inbox_tags": True}, + format="json", + ) + self.document.refresh_from_db() + tags = set(self.document.tags.values_list("pk", flat=True)) + assert tags == {inbox.pk, nested.pk} + def test_bulk_edit_respects_hierarchy(self) -> None: bulk_edit.add_tag([self.document.pk], self.child.pk) self.document.refresh_from_db()