🐛(backend) compute ancestor_links in get_abilities if needed

The refactor made in the tree view caching the ancestors_links to not
compute them again in the document.get_abilities method lead to a bug.
If the get_abilities method is called without ancestors_links, then they
are computed on all the ancestors but not from the highest readable
ancestor for the current user. We have to compute them with this
constraint.
This commit is contained in:
Manuel Raynaud
2025-03-24 10:46:40 +01:00
parent c882f1386c
commit a1914c6259
5 changed files with 90 additions and 4 deletions

View File

@@ -16,6 +16,10 @@ and this project adheres to
- ♻️(frontend) Integrate UI kit #783
## Fixed
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
## [2.6.0] - 2025-03-21
## Added

View File

@@ -846,14 +846,15 @@ class DocumentViewSet(
)
# Get the highest readable ancestor
highest_readable = ancestors.readable_per_se(request.user).only("depth").first()
highest_readable = (
ancestors.readable_per_se(request.user).only("depth", "path").first()
)
if highest_readable is None:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
)
paths_links_mapping = {}
ancestors_links = []
children_clause = db.Q()
@@ -876,6 +877,17 @@ class DocumentViewSet(
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
queryset = queryset.order_by("path")
# Annotate if the current document is the highest ancestor for the user
queryset = queryset.annotate(
is_highest_ancestor_for_user=db.Case(
db.When(
path=db.Value(highest_readable.path),
then=db.Value(True),
),
default=db.Value(False),
output_field=db.BooleanField(),
)
)
queryset = self.annotate_user_roles(queryset)
queryset = self.annotate_is_favorite(queryset)

View File

@@ -754,6 +754,32 @@ class Document(MP_Node, BaseModel):
return dict(links_definitions) # Convert defaultdict back to a normal dict
def compute_ancestors_links(self, user):
"""
Compute the ancestors links for the current document up to the highest readable ancestor.
"""
ancestors = (
(self.get_ancestors() | self._meta.model.objects.filter(pk=self.pk))
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
highest_readable = ancestors.readable_per_se(user).only("depth").first()
if highest_readable is None:
return []
ancestors_links = []
paths_links_mapping = {}
for ancestor in ancestors.filter(depth__gte=highest_readable.depth):
ancestors_links.append(
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
)
paths_links_mapping[ancestor.path] = ancestors_links.copy()
ancestors_links = paths_links_mapping.get(self.path[: -self.steplen], [])
return ancestors_links
def get_abilities(self, user, ancestors_links=None):
"""
Compute and return abilities for a given user on the document.
@@ -761,7 +787,7 @@ class Document(MP_Node, BaseModel):
if self.depth <= 1 or getattr(self, "is_highest_ancestor_for_user", False):
ancestors_links = []
elif ancestors_links is None:
ancestors_links = self.get_ancestors().values("link_reach", "link_role")
ancestors_links = self.compute_ancestors_links(user=user)
roles = set(
self.get_roles(user)

View File

@@ -789,7 +789,7 @@ def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
)
expected_roles = {access.role for access in accesses}
with django_assert_max_num_queries(12):
with django_assert_max_num_queries(14):
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
assert response.status_code == 200

View File

@@ -1305,3 +1305,47 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
def test_models_documents_get_select_options(ancestors_links, select_options):
"""Validate that the "get_select_options" method operates as expected."""
assert models.LinkReachChoices.get_select_options(ancestors_links) == select_options
def test_models_documents_compute_ancestors_links_no_highest_readable():
"""Test the compute_ancestors_links method."""
document = factories.DocumentFactory(link_reach="public")
assert document.compute_ancestors_links(user=AnonymousUser()) == []
def test_models_documents_compute_ancestors_links_highest_readable(
django_assert_num_queries,
):
"""Test the compute_ancestors_links method."""
user = factories.UserFactory()
other_user = factories.UserFactory()
root = factories.DocumentFactory(
link_reach="restricted", link_role="reader", users=[user]
)
factories.DocumentFactory(
parent=root, link_reach="public", link_role="reader", users=[user]
)
child2 = factories.DocumentFactory(
parent=root,
link_reach="authenticated",
link_role="editor",
users=[user, other_user],
)
child3 = factories.DocumentFactory(
parent=child2,
link_reach="authenticated",
link_role="reader",
users=[user, other_user],
)
with django_assert_num_queries(2):
assert child3.compute_ancestors_links(user=user) == [
{"link_reach": root.link_reach, "link_role": root.link_role},
{"link_reach": child2.link_reach, "link_role": child2.link_role},
]
with django_assert_num_queries(2):
assert child3.compute_ancestors_links(user=other_user) == [
{"link_reach": child2.link_reach, "link_role": child2.link_role},
]