From 11dfc9ff035ff219b5e718441bcca06a463d27fc Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 28 Jul 2025 17:38:27 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20is=5Fmasked=20to=20d?= =?UTF-8?q?ocument=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We added the `is_masked` annotation to the document view to indicate whether a document is masked for the current user. This will allow the frontend to handle masked documents appropriately in the UI. --- src/backend/core/api/serializers.py | 4 ++++ src/backend/core/api/viewsets.py | 4 +++- src/backend/core/models.py | 12 +++++++++++ .../test_api_documents_children_list.py | 14 +++++++++++++ .../test_api_documents_descendants.py | 21 +++++++++++++++++++ .../documents/test_api_documents_list.py | 4 ++++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 83afc260..b8fb80b7 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -66,6 +66,7 @@ class ListDocumentSerializer(serializers.ModelSerializer): """Serialize documents with limited fields for display in lists.""" is_favorite = serializers.BooleanField(read_only=True) + is_masked = serializers.BooleanField(read_only=True) nb_accesses_ancestors = serializers.IntegerField(read_only=True) nb_accesses_direct = serializers.IntegerField(read_only=True) user_role = serializers.SerializerMethodField(read_only=True) @@ -85,6 +86,7 @@ class ListDocumentSerializer(serializers.ModelSerializer): "depth", "excerpt", "is_favorite", + "is_masked", "link_role", "link_reach", "nb_accesses_ancestors", @@ -107,6 +109,7 @@ class ListDocumentSerializer(serializers.ModelSerializer): "depth", "excerpt", "is_favorite", + "is_masked", "link_role", "link_reach", "nb_accesses_ancestors", @@ -176,6 +179,7 @@ class DocumentSerializer(ListDocumentSerializer): "depth", "excerpt", "is_favorite", + "is_masked", "link_role", "link_reach", "nb_accesses_ancestors", diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index ee0c594e..918c4831 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -405,6 +405,7 @@ class DocumentViewSet( queryset = super().filter_queryset(queryset) user = self.request.user queryset = queryset.annotate_is_favorite(user) + queryset = queryset.annotate_is_masked(user) queryset = queryset.annotate_user_roles(user) return queryset @@ -453,8 +454,9 @@ class DocumentViewSet( ) queryset = queryset.filter(path__in=root_paths) - # Annotate favorite status and filter if applicable as late as possible + # Annotate favorite and masked status and filter if applicable as late as possible queryset = queryset.annotate_is_favorite(user) + queryset = queryset.annotate_is_masked(user) for field in ["is_favorite", "is_masked"]: queryset = filterset.filters[field].filter(queryset, filter_data[field]) diff --git a/src/backend/core/models.py b/src/backend/core/models.py index a1182964..d4c02d27 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -326,6 +326,18 @@ class DocumentQuerySet(MP_NodeQuerySet): return self.annotate(is_favorite=models.Value(False)) + def annotate_is_masked(self, user): + """ + Annotate document queryset with the masked status for the current user. + """ + if user.is_authenticated: + masked_exists_subquery = LinkTrace.objects.filter( + document_id=models.OuterRef("pk"), user=user, is_masked=True + ) + return self.annotate(is_masked=models.Exists(masked_exists_subquery)) + + return self.annotate(is_masked=models.Value(False)) + def annotate_user_roles(self, user): """ Annotate document queryset with the roles of the current user diff --git a/src/backend/core/tests/documents/test_api_documents_children_list.py b/src/backend/core/tests/documents/test_api_documents_children_list.py index 19bcfd19..20098d04 100644 --- a/src/backend/core/tests/documents/test_api_documents_children_list.py +++ b/src/backend/core/tests/documents/test_api_documents_children_list.py @@ -45,6 +45,7 @@ def test_api_documents_children_list_anonymous_public_standalone( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -67,6 +68,7 @@ def test_api_documents_children_list_anonymous_public_standalone( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -119,6 +121,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -141,6 +144,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -212,6 +216,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -234,6 +239,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -291,6 +297,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -313,6 +320,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -397,6 +405,7 @@ def test_api_documents_children_list_authenticated_related_direct( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -419,6 +428,7 @@ def test_api_documents_children_list_authenticated_related_direct( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -479,6 +489,7 @@ def test_api_documents_children_list_authenticated_related_parent( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -501,6 +512,7 @@ def test_api_documents_children_list_authenticated_related_parent( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -613,6 +625,7 @@ def test_api_documents_children_list_authenticated_related_team_members( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -635,6 +648,7 @@ def test_api_documents_children_list_authenticated_related_team_members( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, diff --git a/src/backend/core/tests/documents/test_api_documents_descendants.py b/src/backend/core/tests/documents/test_api_documents_descendants.py index bd2785a7..4fa08f68 100644 --- a/src/backend/core/tests/documents/test_api_documents_descendants.py +++ b/src/backend/core/tests/documents/test_api_documents_descendants.py @@ -42,6 +42,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -66,6 +67,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -88,6 +90,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -139,6 +142,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -161,6 +165,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -183,6 +188,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -255,6 +261,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -277,6 +284,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -299,6 +307,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -356,6 +365,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -378,6 +388,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -400,6 +411,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -478,6 +490,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -500,6 +513,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -522,6 +536,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -580,6 +595,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -602,6 +618,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -624,6 +641,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -728,6 +746,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -750,6 +769,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -772,6 +792,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, diff --git a/src/backend/core/tests/documents/test_api_documents_list.py b/src/backend/core/tests/documents/test_api_documents_list.py index cfaa3e0a..2fc9e85e 100644 --- a/src/backend/core/tests/documents/test_api_documents_list.py +++ b/src/backend/core/tests/documents/test_api_documents_list.py @@ -72,6 +72,7 @@ def test_api_documents_list_format(): "depth": 1, "excerpt": document.excerpt, "is_favorite": True, + "is_masked": False, "link_reach": document.link_reach, "link_role": document.link_role, "nb_accesses_ancestors": 3, @@ -408,6 +409,7 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries assert len(results) == 5 assert all(result["is_favorite"] is False for result in results) + assert all(result["is_masked"] is False for result in results) # Mark documents as favorite and check results again for document in special_documents: @@ -427,3 +429,5 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries assert result["is_favorite"] is True else: assert result["is_favorite"] is False + # All documents should be unmasked in this test + assert result["is_masked"] is False