(backend) expose restriction targets in the items API

Restrictions expose their target's id, title and a can_access flag so
the frontend can grey out entries pointing to folders the user
cannot open. The children listing prefetches the viewer accesses to
keep the query count flat, and the tree includes restriction entries.
This commit is contained in:
Nicolas Clerc
2026-09-04 15:29:18 +02:00
parent 140cc80338
commit cd3e4e4a0b
10 changed files with 299 additions and 3 deletions
+47
View File
@@ -11,6 +11,7 @@ from os.path import splitext
from urllib.parse import quote
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -221,6 +222,48 @@ class ItemAccessLightSerializer(ItemAccessSerializer):
]
class RestrictionTargetSerializer(serializers.ModelSerializer):
"""Serialize the restricted folder a restriction points to."""
deleted = serializers.SerializerMethodField()
can_access = serializers.SerializerMethodField()
is_restricted = serializers.SerializerMethodField()
class Meta:
model = models.Item
fields = ["id", "title", "is_restricted", "deleted", "can_access"]
read_only_fields = ["id", "title", "is_restricted", "deleted", "can_access"]
def get_deleted(self, target) -> bool:
"""Return whether the target is in the trash."""
return target.deleted_at is not None
def get_is_restricted(self, target) -> bool: # pylint: disable=unused-argument
"""Return True: the target of an existing restriction is restricted by definition."""
return True
def get_can_access(self, target) -> bool:
"""Return whether the request user can open the target."""
request = self.context.get("request")
user = request.user if request else None
if user is not None and user.is_authenticated:
accesses = getattr(target, "viewer_accesses", None)
if accesses is None:
has_access = models.ItemAccess.objects.filter(
Q(user=user) | Q(team__in=user.teams),
item=target,
).exists()
else:
has_access = bool(accesses)
if has_access:
return True
return target.link_reach == LinkReachChoices.PUBLIC or (
target.link_reach == LinkReachChoices.AUTHENTICATED
and user is not None
and user.is_authenticated
)
class ListItemSerializer(serializers.ModelSerializer):
"""Serialize items with limited fields for display in lists."""
@@ -234,6 +277,7 @@ class ListItemSerializer(serializers.ModelSerializer):
creator = UserLightSerializer(read_only=True)
hard_delete_at = serializers.SerializerMethodField(read_only=True)
is_wopi_supported = serializers.SerializerMethodField()
target = RestrictionTargetSerializer(read_only=True, allow_null=True)
class Meta:
model = models.Item
@@ -255,6 +299,7 @@ class ListItemSerializer(serializers.ModelSerializer):
"numchild",
"numchild_folder",
"path",
"target",
"title",
"updated_at",
"user_role",
@@ -288,6 +333,7 @@ class ListItemSerializer(serializers.ModelSerializer):
"link_reach",
"nb_accesses",
"path",
"target",
"updated_at",
"user_role",
"type",
@@ -487,6 +533,7 @@ class ItemSerializer(ListItemSerializer):
"numchild",
"numchild_folder",
"path",
"target",
"title",
"updated_at",
"user_role",
+17 -3
View File
@@ -1144,10 +1144,20 @@ class ItemViewSet(
# GET: List children
queryset = (
item.children()
.select_related("creator")
.select_related("creator", "target")
.annotate_has_restriction()
.filter(deleted_at__isnull=True)
)
if request.user.is_authenticated:
queryset = queryset.prefetch_related(
db.Prefetch(
"target__accesses",
queryset=models.ItemAccess.objects.filter(
db.Q(user=request.user) | db.Q(team__in=request.user.teams)
),
to_attr="viewer_accesses",
)
)
queryset = self._filter_suspicious_items(queryset, request.user)
queryset = self._exclude_pending_items(queryset)
queryset = self.filter_queryset(queryset)
@@ -1244,9 +1254,13 @@ class ItemViewSet(
paths_links_mapping[str(ancestor.path)] = ancestors_links.copy()
tree = (
self.queryset.select_related("creator")
self.queryset.select_related("creator", "target")
.annotate_has_restriction()
.filter(clause, type=models.ItemTypeChoices.FOLDER, deleted_at__isnull=True)
.filter(
clause,
type__in=[models.ItemTypeChoices.FOLDER, models.ItemTypeChoices.RESTRICTION],
deleted_at__isnull=True,
)
.order_by("created_at")
)
@@ -48,6 +48,7 @@ def test_api_items_children_list_anonymous_public_standalone():
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -93,6 +94,7 @@ def test_api_items_children_list_anonymous_public_standalone():
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -178,6 +180,7 @@ def test_api_items_children_list_anonymous_public_parent():
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -217,6 +220,7 @@ def test_api_items_children_list_anonymous_public_parent():
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -310,6 +314,7 @@ def test_api_items_children_list_authenticated_unrelated_public_or_authenticated
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -355,6 +360,7 @@ def test_api_items_children_list_authenticated_unrelated_public_or_authenticated
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -437,6 +443,7 @@ def test_api_items_children_list_authenticated_public_or_authenticated_parent(
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -482,6 +489,7 @@ def test_api_items_children_list_authenticated_public_or_authenticated_parent(
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -589,6 +597,7 @@ def test_api_items_children_list_authenticated_related_direct():
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -634,6 +643,7 @@ def test_api_items_children_list_authenticated_related_direct():
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -719,6 +729,7 @@ def test_api_items_children_list_authenticated_related_parent():
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -764,6 +775,7 @@ def test_api_items_children_list_authenticated_related_parent():
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -906,6 +918,7 @@ def test_api_items_children_list_authenticated_related_team_members(
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -951,6 +964,7 @@ def test_api_items_children_list_authenticated_related_team_members(
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -1034,6 +1048,7 @@ def test_api_items_children_list_filter_type():
"id": str(child1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -1088,6 +1103,7 @@ def test_api_items_children_list_filter_type():
"id": str(child2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -1396,6 +1412,7 @@ def test_api_items_children_list_computed_link_reach_and_role():
"id": str(item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "restricted",
"link_role": "reader",
"numchild": 1,
@@ -1445,6 +1462,7 @@ def test_api_items_children_list_computed_link_reach_and_role():
"id": str(child.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "public",
"link_role": "editor",
"numchild": 0,
@@ -107,6 +107,7 @@ def test_api_items_list_format():
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": item3.link_reach,
"link_role": item3.link_role,
"nb_accesses": 0,
@@ -146,6 +147,7 @@ def test_api_items_list_format():
"depth": 1,
"is_favorite": True,
"is_restricted": False,
"target": None,
"link_reach": item2.link_reach,
"link_role": item2.link_role,
"nb_accesses": 3,
@@ -185,6 +187,7 @@ def test_api_items_list_format():
"depth": 1,
"is_favorite": True,
"is_restricted": False,
"target": None,
"link_reach": item.link_reach,
"link_role": item.link_role,
"nb_accesses": 3,
@@ -0,0 +1,177 @@
"""Tests for the restriction target details in the items API."""
from django.db import connection
from django.test.utils import CaptureQueriesContext
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
def _create_restricted_folder(parent, user):
"""Create a folder under parent and restrict it as user."""
folder = factories.ItemFactory(parent=parent, type=models.ItemTypeChoices.FOLDER)
factories.UserItemAccessFactory(item=folder, user=user, role="owner")
return folder.restrict(user)
def test_api_items_restrictions_children_list_exposes_target():
"""The children listing exposes the restriction target, greyed for excluded users."""
parent_owner = factories.UserFactory()
owner = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(parent_owner, "owner")],
)
folder = _create_restricted_folder(parent, owner)
restriction = folder.restriction
client = APIClient()
client.force_login(parent_owner)
response = client.get(f"/api/v1.0/items/{parent.id!s}/children/")
assert response.status_code == 200
results = {result["id"]: result for result in response.json()["results"]}
payload = results[str(restriction.id)]
assert payload["type"] == "restriction"
assert payload["target"] == {
"id": str(folder.id),
"title": folder.title,
"is_restricted": True,
"deleted": False,
"can_access": False,
}
def test_api_items_restrictions_children_list_target_accessible():
"""The target is accessible for a user holding an explicit access on it."""
user = factories.UserFactory()
owner = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "reader")],
)
folder = _create_restricted_folder(parent, owner)
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/items/{parent.id!s}/children/")
assert response.status_code == 200
results = {result["id"]: result for result in response.json()["results"]}
assert results[str(folder.restriction.id)]["target"]["can_access"] is True
def test_api_items_restrictions_children_list_target_accessible_via_link():
"""A public link reach on the target grants access through the restriction."""
parent_owner = factories.UserFactory()
owner = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(parent_owner, "owner")],
)
folder = _create_restricted_folder(parent, owner)
models.Item.objects.filter(pk=folder.pk).update(link_reach="public")
client = APIClient()
client.force_login(parent_owner)
response = client.get(f"/api/v1.0/items/{parent.id!s}/children/")
assert response.status_code == 200
results = {result["id"]: result for result in response.json()["results"]}
assert results[str(folder.restriction.id)]["target"]["can_access"] is True
def test_api_items_restrictions_retrieve_exposes_target():
"""Retrieving a restriction exposes its target."""
user = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "owner")],
)
folder = _create_restricted_folder(parent, user)
restriction = folder.restriction
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/items/{restriction.id!s}/")
assert response.status_code == 200
assert response.json()["target"]["id"] == str(folder.id)
def test_api_items_restrictions_non_restriction_target_is_none():
"""Regular items expose a null target."""
user = factories.UserFactory()
folder = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "owner")],
)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/items/{folder.id!s}/")
assert response.status_code == 200
assert response.json()["target"] is None
def test_api_items_restrictions_children_list_constant_queries():
"""The number of queries does not grow with the number of restrictions listed."""
parent_owner = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(parent_owner, "owner")],
)
_create_restricted_folder(parent, factories.UserFactory())
client = APIClient()
client.force_login(parent_owner)
# Warm the nb_accesses cache so both measures run in the same conditions
client.get(f"/api/v1.0/items/{parent.id!s}/children/")
with CaptureQueriesContext(connection) as single:
response = client.get(f"/api/v1.0/items/{parent.id!s}/children/")
assert response.status_code == 200
for _ in range(3):
_create_restricted_folder(parent, factories.UserFactory())
with CaptureQueriesContext(connection) as many:
response = client.get(f"/api/v1.0/items/{parent.id!s}/children/")
assert response.status_code == 200
assert len(response.json()["results"]) == 4
assert len(many) == len(single)
def test_api_items_restrictions_tree_includes_restrictions():
"""The tree endpoint includes restriction entries so excluded users see them."""
parent_owner = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(parent_owner, "owner")],
)
sibling = factories.ItemFactory(parent=parent, type=models.ItemTypeChoices.FOLDER)
folder = _create_restricted_folder(parent, factories.UserFactory())
restriction = folder.restriction
client = APIClient()
client.force_login(parent_owner)
response = client.get(f"/api/v1.0/items/{sibling.id!s}/tree/")
assert response.status_code == 200
tree = response.json()
children_ids = {child["id"] for child in tree["children"]}
assert str(restriction.id) in children_ids
@@ -44,6 +44,7 @@ def test_api_items_retrieve_anonymous_public_standalone():
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "public",
"link_role": item.link_role,
"nb_accesses": 0,
@@ -102,6 +103,7 @@ def test_api_items_retrieve_anonymous_public_parent():
"depth": 3,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": item.link_reach,
"link_role": item.link_role,
"nb_accesses": 0,
@@ -207,6 +209,7 @@ def test_api_items_retrieve_authenticated_unrelated_public_or_authenticated(reac
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": reach,
"link_role": item.link_role,
"nb_accesses": 0,
@@ -271,6 +274,7 @@ def test_api_items_retrieve_authenticated_public_or_authenticated_parent(reach):
"depth": 3,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": item.link_reach,
"link_role": item.link_role,
"nb_accesses": 0,
@@ -413,6 +417,7 @@ def test_api_items_retrieve_authenticated_related_direct():
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": item.link_reach,
"link_role": item.link_role,
"nb_accesses": 2,
@@ -481,6 +486,7 @@ def test_api_items_retrieve_authenticated_related_parent():
"depth": 3,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "restricted",
"link_role": item.link_role,
"nb_accesses": 2,
@@ -659,6 +665,7 @@ def test_api_items_retrieve_authenticated_related_team_members(teams, role, mock
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "restricted",
"link_role": item.link_role,
"nb_accesses": 5,
@@ -735,6 +742,7 @@ def test_api_items_retrieve_authenticated_related_team_administrators(teams, rol
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "restricted",
"link_role": item.link_role,
"nb_accesses": 5,
@@ -811,6 +819,7 @@ def test_api_items_retrieve_authenticated_related_team_owners(teams, mock_user_t
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "restricted",
"link_role": item.link_role,
"nb_accesses": 5,
@@ -1211,6 +1220,7 @@ def test_api_items_retrieve_file_with_url_property(upload_state):
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "public",
"link_role": item.link_role,
"nb_accesses": 1,
@@ -1285,6 +1295,7 @@ def test_api_items_retrieve_file_with_url_property_non_previewable(upload_state)
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "public",
"link_role": item.link_role,
"nb_accesses": 1,
@@ -1349,6 +1360,7 @@ def test_api_items_retrieve_file_with_url_property_with_spaces():
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "public",
"link_role": item.link_role,
"nb_accesses": 1,
@@ -1495,6 +1507,7 @@ def test_api_items_retrieve_file_analysing_not_creator():
"depth": 1,
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": "public",
"link_role": item.link_role,
@@ -73,6 +73,7 @@ def test_api_items_search_authenticated_without_filters():
"id": str(top_parent.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": top_parent.link_reach,
"link_role": top_parent.link_role,
@@ -113,6 +114,7 @@ def test_api_items_search_authenticated_without_filters():
"id": str(parent.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
@@ -142,6 +144,7 @@ def test_api_items_search_authenticated_without_filters():
"id": str(top_parent.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": top_parent.link_reach,
"link_role": top_parent.link_role,
@@ -193,6 +196,7 @@ def test_api_items_search_authenticated_without_filters():
"id": str(children.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": children.link_reach,
"link_role": children.link_role,
@@ -222,6 +226,7 @@ def test_api_items_search_authenticated_without_filters():
"id": str(top_parent.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": top_parent.link_reach,
"link_role": top_parent.link_role,
@@ -261,6 +266,7 @@ def test_api_items_search_authenticated_without_filters():
"id": str(parent.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
@@ -83,6 +83,7 @@ def test_api_items_search_authenticated_fulltext_query(indexer_settings):
"id": str(item_b.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": item_b.link_reach,
"link_role": item_b.link_role,
@@ -121,6 +122,7 @@ def test_api_items_search_authenticated_fulltext_query(indexer_settings):
"hard_delete_at": None,
"id": str(folder.id),
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": folder.link_reach,
"link_role": folder.link_role,
@@ -162,6 +164,7 @@ def test_api_items_search_authenticated_fulltext_query(indexer_settings):
"id": str(item_c.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": item_c.link_reach,
"link_role": item_c.link_role,
@@ -200,6 +203,7 @@ def test_api_items_search_authenticated_fulltext_query(indexer_settings):
"hard_delete_at": None,
"id": str(folder.id),
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
"link_reach": folder.link_reach,
"link_role": folder.link_role,
@@ -104,6 +104,7 @@ def test_api_items_trashbin_format(settings):
"description": None,
"hard_delete_at": ((now + timedelta(days=30)).isoformat()),
"is_restricted": False,
"target": None,
"is_wopi_supported": False,
}
@@ -159,6 +159,7 @@ def test_items_api_anonymous_to_a_public_tree_structure():
"id": str(level2_1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level2_1.link_reach,
"link_role": level2_1.link_role,
"nb_accesses": 0,
@@ -199,6 +200,7 @@ def test_items_api_anonymous_to_a_public_tree_structure():
"id": str(level2_2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level2_2.link_reach,
"link_role": level2_2.link_role,
"nb_accesses": 0,
@@ -233,6 +235,7 @@ def test_items_api_anonymous_to_a_public_tree_structure():
"id": str(level1_2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level1_2.link_reach,
"link_role": level1_2.link_role,
"nb_accesses": 0,
@@ -393,6 +396,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(level2_1.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level2_1.item.link_reach,
"link_role": level2_1.item.link_role,
"nb_accesses": 3,
@@ -442,6 +446,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(level3_1.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level3_1.item.link_reach,
"link_role": level3_1.item.link_role,
"nb_accesses": 4,
@@ -481,6 +486,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(level2_2.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level2_2.item.link_reach,
"link_role": level2_2.item.link_role,
"nb_accesses": 3,
@@ -515,6 +521,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(level1_1.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level1_1.item.link_reach,
"link_role": level1_1.item.link_role,
"nb_accesses": 2,
@@ -555,6 +562,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(level1_2.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level1_2.item.link_reach,
"link_role": level1_2.item.link_role,
"nb_accesses": 2,
@@ -595,6 +603,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(level1_3.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": level1_3.item.link_reach,
"link_role": level1_3.item.link_role,
"nb_accesses": 2,
@@ -629,6 +638,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries):
"id": str(root.item.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": root.item.link_reach,
"link_role": root.item.link_role,
"nb_accesses": 1,
@@ -746,6 +756,7 @@ def test_api_items_tree_authenticated_with_access_authenticated():
"id": str(level1_1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "authenticated",
"link_role": level1_1.link_role,
"nb_accesses": 0,
@@ -791,6 +802,7 @@ def test_api_items_tree_authenticated_with_access_authenticated():
"id": str(level2_1.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "authenticated",
"link_role": level2_1.link_role,
"nb_accesses": 0,
@@ -831,6 +843,7 @@ def test_api_items_tree_authenticated_with_access_authenticated():
"id": str(level2_2.id),
"is_favorite": False,
"is_restricted": False,
"target": None,
"link_reach": "authenticated",
"link_role": level2_2.link_role,
"nb_accesses": 0,