diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 1d805624..af269a94 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -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", diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index c21ffa57..fd6ea0ec 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -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") ) diff --git a/src/backend/core/tests/items/test_api_items_children_list.py b/src/backend/core/tests/items/test_api_items_children_list.py index 58476e04..2d824b59 100644 --- a/src/backend/core/tests/items/test_api_items_children_list.py +++ b/src/backend/core/tests/items/test_api_items_children_list.py @@ -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, diff --git a/src/backend/core/tests/items/test_api_items_list.py b/src/backend/core/tests/items/test_api_items_list.py index b4898f17..0fadd238 100644 --- a/src/backend/core/tests/items/test_api_items_list.py +++ b/src/backend/core/tests/items/test_api_items_list.py @@ -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, diff --git a/src/backend/core/tests/items/test_api_items_restrictions.py b/src/backend/core/tests/items/test_api_items_restrictions.py new file mode 100644 index 00000000..495cafb7 --- /dev/null +++ b/src/backend/core/tests/items/test_api_items_restrictions.py @@ -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 diff --git a/src/backend/core/tests/items/test_api_items_retrieve.py b/src/backend/core/tests/items/test_api_items_retrieve.py index 7151987a..d687824a 100644 --- a/src/backend/core/tests/items/test_api_items_retrieve.py +++ b/src/backend/core/tests/items/test_api_items_retrieve.py @@ -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, diff --git a/src/backend/core/tests/items/test_api_items_search.py b/src/backend/core/tests/items/test_api_items_search.py index 593dc5f1..1570c5a6 100644 --- a/src/backend/core/tests/items/test_api_items_search.py +++ b/src/backend/core/tests/items/test_api_items_search.py @@ -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, diff --git a/src/backend/core/tests/items/test_api_items_search_fulltext.py b/src/backend/core/tests/items/test_api_items_search_fulltext.py index 42d4185b..3ab933a9 100644 --- a/src/backend/core/tests/items/test_api_items_search_fulltext.py +++ b/src/backend/core/tests/items/test_api_items_search_fulltext.py @@ -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, diff --git a/src/backend/core/tests/items/test_api_items_trashbin.py b/src/backend/core/tests/items/test_api_items_trashbin.py index cf447800..49aacb19 100644 --- a/src/backend/core/tests/items/test_api_items_trashbin.py +++ b/src/backend/core/tests/items/test_api_items_trashbin.py @@ -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, } diff --git a/src/backend/core/tests/items/test_api_items_tree.py b/src/backend/core/tests/items/test_api_items_tree.py index f2eb0472..8a099e0e 100644 --- a/src/backend/core/tests/items/test_api_items_tree.py +++ b/src/backend/core/tests/items/test_api_items_tree.py @@ -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,