diff --git a/src/backend/core/api/utils.py b/src/backend/core/api/utils.py index 9fd059e1..98dc6548 100644 --- a/src/backend/core/api/utils.py +++ b/src/backend/core/api/utils.py @@ -11,6 +11,35 @@ import botocore from rest_framework.throttling import BaseThrottle +def nest_tree(flat_list, steplen): + """ + Convert a flat list of serialized documents into a nested tree making advantage + of the`path` field and its step length. + """ + node_dict = {} + roots = [] + + # Sort the flat list by path to ensure parent nodes are processed first + flat_list.sort(key=lambda x: x["path"]) + + for node in flat_list: + node["children"] = [] # Initialize children list + node_dict[node["path"]] = node + + # Determine parent path + parent_path = node["path"][:-steplen] + + if parent_path in node_dict: + node_dict[parent_path]["children"].append(node) + else: + roots.append(node) # Collect root nodes + + if len(roots) > 1: + raise ValueError("More than one root element detected.") + + return roots[0] if roots else None + + def filter_root_paths(paths, skip_sorting=False): """ Filters root paths from a list of paths representing a tree structure. diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index c2f98623..2a7d90f1 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -773,22 +773,18 @@ class DocumentViewSet( methods=["get"], ordering=["path"], ) - def tree(self, request, *args, **kwargs): + def tree(self, request, pk, *args, **kwargs): """ List ancestors tree above the document. What we need to display is the tree structure opened for the current document. """ - lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field - filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} - current_document = ( - self.queryset.filter(**filter_kwargs).only("depth", "path").first() - ) - - if current_document is None: - raise Http404 + try: + current_document = self.queryset.only("depth", "path").get(pk=pk) + except models.Document.DoesNotExist as excpt: + raise drf.exceptions.NotFound from excpt ancestors = ( - (current_document.get_ancestors() | self.queryset.filter(**filter_kwargs)) + (current_document.get_ancestors() | self.queryset.filter(pk=pk)) .filter(ancestors_deleted_at__isnull=True) .order_by("path") ) @@ -830,8 +826,9 @@ class DocumentViewSet( "ancestors_links_definitions": ancestors_links_definitions, }, ) - - return drf.response.Response(serializer.data) + return drf.response.Response( + utils.nest_tree(serializer.data, self.queryset.model.steplen) + ) @drf.decorators.action(detail=True, methods=["get"], url_path="versions") def versions_list(self, request, *args, **kwargs): diff --git a/src/backend/core/models.py b/src/backend/core/models.py index c20c7a18..406d56cb 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -29,7 +29,7 @@ from django.utils.translation import gettext_lazy as _ from botocore.exceptions import ClientError from rest_framework.exceptions import ValidationError from timezone_field import TimeZoneField -from treebeard.mp_tree import MP_Node +from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet logger = getLogger(__name__) @@ -367,7 +367,7 @@ class BaseAccess(BaseModel): } -class DocumentQuerySet(models.QuerySet): +class DocumentQuerySet(MP_NodeQuerySet): """ Custom queryset for the Document model, providing additional methods to filter documents based on user permissions. @@ -387,10 +387,10 @@ class DocumentQuerySet(models.QuerySet): | ~models.Q(link_reach=LinkReachChoices.RESTRICTED) ) - return self.filter(models.Q(link_reach=LinkReachChoices.PUBLIC)) + return self.filter(link_reach=LinkReachChoices.PUBLIC) -class DocumentManager(models.Manager): +class DocumentManager(MP_NodeManager): """ Custom manager for the Document model, enabling the use of the custom queryset methods directly from the model manager. @@ -619,7 +619,11 @@ class Document(MP_Node, BaseModel): def invalidate_nb_accesses_cache(self): """ Invalidate the cache for number of accesses, including on affected descendants. + Args: + path: can optionally be passed as argument (useful when invalidating cache for a + document we just deleted) """ + for document in Document.objects.filter(path__startswith=self.path).only("id"): cache_key = document.get_nb_accesses_cache_key() cache.delete(cache_key) diff --git a/src/backend/core/tests/documents/test_api_documents_tree.py b/src/backend/core/tests/documents/test_api_documents_tree.py index aea989ab..17c9bb93 100644 --- a/src/backend/core/tests/documents/test_api_documents_tree.py +++ b/src/backend/core/tests/documents/test_api_documents_tree.py @@ -1,6 +1,7 @@ """ Tests for Documents API endpoint in impress's core app: retrieve """ +# pylint: disable=too-many-lines import random @@ -27,76 +28,107 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/tree/") assert response.status_code == 200 - assert response.json() == [ - { - "abilities": parent.get_abilities(AnonymousUser()), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 1, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 0, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": document.get_abilities(AnonymousUser()), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 2, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 0, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": child.get_abilities(AnonymousUser()), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 3, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": sibling.get_abilities(AnonymousUser()), - "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(sibling.creator.id), - "depth": 2, - "excerpt": sibling.excerpt, - "id": str(sibling.id), - "is_favorite": False, - "link_reach": sibling.link_reach, - "link_role": sibling.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": sibling.path, - "title": sibling.title, - "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - ] + assert response.json() == { + "abilities": parent.get_abilities(AnonymousUser()), + "children": [ + { + "abilities": document.get_abilities(AnonymousUser()), + "children": [ + { + "abilities": child.get_abilities(AnonymousUser()), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 3, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), + "depth": 2, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + { + "abilities": sibling1.get_abilities(AnonymousUser()), + "children": [], + "created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(sibling1.creator.id), + "depth": 2, + "excerpt": sibling1.excerpt, + "id": str(sibling1.id), + "is_favorite": False, + "link_reach": sibling1.link_reach, + "link_role": sibling1.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": sibling1.path, + "title": sibling1.title, + "updated_at": sibling1.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + { + "abilities": sibling2.get_abilities(AnonymousUser()), + "children": [], + "created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(sibling2.creator.id), + "depth": 2, + "excerpt": sibling2.excerpt, + "id": str(sibling2.id), + "is_favorite": False, + "link_reach": sibling2.link_reach, + "link_role": sibling2.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": sibling2.path, + "title": sibling2.title, + "updated_at": sibling2.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 1, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 3, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + } def test_api_documents_tree_list_anonymous_public_parent(): @@ -129,114 +161,139 @@ def test_api_documents_tree_list_anonymous_public_parent(): response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/tree/") assert response.status_code == 200 - assert response.json() == [ - { - "abilities": grand_parent.get_abilities(AnonymousUser()), - "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(grand_parent.creator.id), - "depth": 2, - "excerpt": grand_parent.excerpt, - "id": str(grand_parent.id), - "is_favorite": False, - "link_reach": grand_parent.link_reach, - "link_role": grand_parent.link_role, - "numchild": 2, - "nb_accesses": 0, - "path": grand_parent.path, - "title": grand_parent.title, - "updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": parent.get_abilities(AnonymousUser()), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 3, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 0, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": document.get_abilities(AnonymousUser()), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 4, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 0, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": child.get_abilities(AnonymousUser()), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 5, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": document_sibling.get_abilities(AnonymousUser()), - "created_at": document_sibling.created_at.isoformat().replace( - "+00:00", "Z" - ), - "creator": str(document_sibling.creator.id), - "depth": 4, - "excerpt": document_sibling.excerpt, - "id": str(document_sibling.id), - "is_favorite": False, - "link_reach": document_sibling.link_reach, - "link_role": document_sibling.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": document_sibling.path, - "title": document_sibling.title, - "updated_at": document_sibling.updated_at.isoformat().replace( - "+00:00", "Z" - ), - "user_roles": [], - }, - { - "abilities": parent_sibling.get_abilities(AnonymousUser()), - "created_at": parent_sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent_sibling.creator.id), - "depth": 3, - "excerpt": parent_sibling.excerpt, - "id": str(parent_sibling.id), - "is_favorite": False, - "link_reach": parent_sibling.link_reach, - "link_role": parent_sibling.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": parent_sibling.path, - "title": parent_sibling.title, - "updated_at": parent_sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - ] + assert response.json() == { + "abilities": grand_parent.get_abilities(AnonymousUser()), + "children": [ + { + "abilities": parent.get_abilities(AnonymousUser()), + "children": [ + { + "abilities": document.get_abilities(AnonymousUser()), + "children": [ + { + "abilities": child.get_abilities(AnonymousUser()), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 5, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": document.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(document.creator.id), + "depth": 4, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + { + "abilities": document_sibling.get_abilities(AnonymousUser()), + "children": [], + "created_at": document_sibling.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(document_sibling.creator.id), + "depth": 4, + "excerpt": document_sibling.excerpt, + "id": str(document_sibling.id), + "is_favorite": False, + "link_reach": document_sibling.link_reach, + "link_role": document_sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": document_sibling.path, + "title": document_sibling.title, + "updated_at": document_sibling.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 3, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + { + "abilities": parent_sibling.get_abilities(AnonymousUser()), + "children": [], + "created_at": parent_sibling.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(parent_sibling.creator.id), + "depth": 3, + "excerpt": parent_sibling.excerpt, + "id": str(parent_sibling.id), + "is_favorite": False, + "link_reach": parent_sibling.link_reach, + "link_role": parent_sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": parent_sibling.path, + "title": parent_sibling.title, + "updated_at": parent_sibling.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(grand_parent.creator.id), + "depth": 2, + "excerpt": grand_parent.excerpt, + "id": str(grand_parent.id), + "is_favorite": False, + "link_reach": grand_parent.link_reach, + "link_role": grand_parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": grand_parent.path, + "title": grand_parent.title, + "updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + } @pytest.mark.parametrize("reach", ["restricted", "authenticated"]) @@ -280,76 +337,88 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") assert response.status_code == 200 - assert response.json() == [ - { - "abilities": parent.get_abilities(user), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 1, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 0, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": document.get_abilities(user), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 2, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 0, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": child.get_abilities(user), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 3, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": sibling.get_abilities(user), - "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(sibling.creator.id), - "depth": 2, - "excerpt": sibling.excerpt, - "id": str(sibling.id), - "is_favorite": False, - "link_reach": sibling.link_reach, - "link_role": sibling.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": sibling.path, - "title": sibling.title, - "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - ] + assert response.json() == { + "abilities": parent.get_abilities(user), + "children": [ + { + "abilities": document.get_abilities(user), + "children": [ + { + "abilities": child.get_abilities(user), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 3, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), + "depth": 2, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + { + "abilities": sibling.get_abilities(user), + "children": [], + "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(sibling.creator.id), + "depth": 2, + "excerpt": sibling.excerpt, + "id": str(sibling.id), + "is_favorite": False, + "link_reach": sibling.link_reach, + "link_role": sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": sibling.path, + "title": sibling.title, + "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 1, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + } @pytest.mark.parametrize("reach", ["public", "authenticated"]) @@ -387,114 +456,139 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") assert response.status_code == 200 - assert response.json() == [ - { - "abilities": grand_parent.get_abilities(user), - "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(grand_parent.creator.id), - "depth": 2, - "excerpt": grand_parent.excerpt, - "id": str(grand_parent.id), - "is_favorite": False, - "link_reach": grand_parent.link_reach, - "link_role": grand_parent.link_role, - "numchild": 2, - "nb_accesses": 0, - "path": grand_parent.path, - "title": grand_parent.title, - "updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": parent.get_abilities(user), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 3, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 0, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": document.get_abilities(user), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 4, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 0, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": child.get_abilities(user), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 5, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - { - "abilities": document_sibling.get_abilities(user), - "created_at": document_sibling.created_at.isoformat().replace( - "+00:00", "Z" - ), - "creator": str(document_sibling.creator.id), - "depth": 4, - "excerpt": document_sibling.excerpt, - "id": str(document_sibling.id), - "is_favorite": False, - "link_reach": document_sibling.link_reach, - "link_role": document_sibling.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": document_sibling.path, - "title": document_sibling.title, - "updated_at": document_sibling.updated_at.isoformat().replace( - "+00:00", "Z" - ), - "user_roles": [], - }, - { - "abilities": parent_sibling.get_abilities(user), - "created_at": parent_sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent_sibling.creator.id), - "depth": 3, - "excerpt": parent_sibling.excerpt, - "id": str(parent_sibling.id), - "is_favorite": False, - "link_reach": parent_sibling.link_reach, - "link_role": parent_sibling.link_role, - "numchild": 0, - "nb_accesses": 0, - "path": parent_sibling.path, - "title": parent_sibling.title, - "updated_at": parent_sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [], - }, - ] + assert response.json() == { + "abilities": grand_parent.get_abilities(user), + "children": [ + { + "abilities": parent.get_abilities(user), + "children": [ + { + "abilities": document.get_abilities(user), + "children": [ + { + "abilities": child.get_abilities(user), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 5, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": document.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(document.creator.id), + "depth": 4, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + { + "abilities": document_sibling.get_abilities(user), + "children": [], + "created_at": document_sibling.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(document_sibling.creator.id), + "depth": 4, + "excerpt": document_sibling.excerpt, + "id": str(document_sibling.id), + "is_favorite": False, + "link_reach": document_sibling.link_reach, + "link_role": document_sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": document_sibling.path, + "title": document_sibling.title, + "updated_at": document_sibling.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 3, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + }, + { + "abilities": parent_sibling.get_abilities(user), + "children": [], + "created_at": parent_sibling.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(parent_sibling.creator.id), + "depth": 3, + "excerpt": parent_sibling.excerpt, + "id": str(parent_sibling.id), + "is_favorite": False, + "link_reach": parent_sibling.link_reach, + "link_role": parent_sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": parent_sibling.path, + "title": parent_sibling.title, + "updated_at": parent_sibling.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [], + }, + ], + "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(grand_parent.creator.id), + "depth": 2, + "excerpt": grand_parent.excerpt, + "id": str(grand_parent.id), + "is_favorite": False, + "link_reach": grand_parent.link_reach, + "link_role": grand_parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 0, + "nb_accesses_direct": 0, + "path": grand_parent.path, + "title": grand_parent.title, + "updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [], + } def test_api_documents_tree_list_authenticated_unrelated_restricted(): @@ -541,76 +635,88 @@ def test_api_documents_tree_list_authenticated_related_direct(): f"/api/v1.0/documents/{document.id!s}/tree/", ) assert response.status_code == 200 - assert response.json() == [ - { - "abilities": parent.get_abilities(user), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 1, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 2, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": document.get_abilities(user), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 2, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 2, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": child.get_abilities(user), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 3, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": sibling.get_abilities(user), - "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(sibling.creator.id), - "depth": 2, - "excerpt": sibling.excerpt, - "id": str(sibling.id), - "is_favorite": False, - "link_reach": sibling.link_reach, - "link_role": sibling.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": sibling.path, - "title": sibling.title, - "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - ] + assert response.json() == { + "abilities": parent.get_abilities(user), + "children": [ + { + "abilities": document.get_abilities(user), + "children": [ + { + "abilities": child.get_abilities(user), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 3, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [access.role], + }, + ], + "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), + "depth": 2, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + }, + { + "abilities": sibling.get_abilities(user), + "children": [], + "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(sibling.creator.id), + "depth": 2, + "excerpt": sibling.excerpt, + "id": str(sibling.id), + "is_favorite": False, + "link_reach": sibling.link_reach, + "link_role": sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": sibling.path, + "title": sibling.title, + "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 1, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 2, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + } def test_api_documents_tree_list_authenticated_related_parent(): @@ -622,134 +728,169 @@ def test_api_documents_tree_list_authenticated_related_parent(): client = APIClient() client.force_login(user) - great_grand_parent = factories.DocumentFactory(link_reach="restricted") + great_grand_parent = factories.DocumentFactory( + link_reach="restricted", link_role="reader" + ) grand_parent = factories.DocumentFactory( - link_reach="restricted", parent=great_grand_parent + link_reach="restricted", link_role="reader", parent=great_grand_parent ) access = factories.UserDocumentAccessFactory(document=grand_parent, user=user) factories.UserDocumentAccessFactory(document=grand_parent) factories.DocumentFactory(link_reach="restricted", parent=great_grand_parent) factories.DocumentFactory(link_reach="public", parent=great_grand_parent) - parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted") + parent = factories.DocumentFactory( + parent=grand_parent, link_reach="restricted", link_role="reader" + ) parent_sibling = factories.DocumentFactory( - parent=grand_parent, link_reach="restricted" + parent=grand_parent, link_reach="restricted", link_role="reader" + ) + document = factories.DocumentFactory( + link_reach="restricted", link_role="reader", parent=parent + ) + document_sibling = factories.DocumentFactory( + link_reach="restricted", link_role="reader", parent=parent + ) + child = factories.DocumentFactory( + link_reach="restricted", link_role="reader", parent=document ) - document = factories.DocumentFactory(link_reach="restricted", parent=parent) - document_sibling = factories.DocumentFactory(link_reach="restricted", parent=parent) - child = factories.DocumentFactory(link_reach="restricted", parent=document) response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") assert response.status_code == 200 - assert response.json() == [ - { - "abilities": grand_parent.get_abilities(user), - "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(grand_parent.creator.id), - "depth": 2, - "excerpt": grand_parent.excerpt, - "id": str(grand_parent.id), - "is_favorite": False, - "link_reach": grand_parent.link_reach, - "link_role": grand_parent.link_role, - "numchild": 2, - "nb_accesses": 2, - "path": grand_parent.path, - "title": grand_parent.title, - "updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": parent.get_abilities(user), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 3, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 2, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": document.get_abilities(user), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 4, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 2, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": child.get_abilities(user), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 5, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": document_sibling.get_abilities(user), - "created_at": document_sibling.created_at.isoformat().replace( - "+00:00", "Z" - ), - "creator": str(document_sibling.creator.id), - "depth": 4, - "excerpt": document_sibling.excerpt, - "id": str(document_sibling.id), - "is_favorite": False, - "link_reach": document_sibling.link_reach, - "link_role": document_sibling.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": document_sibling.path, - "title": document_sibling.title, - "updated_at": document_sibling.updated_at.isoformat().replace( - "+00:00", "Z" - ), - "user_roles": [access.role], - }, - { - "abilities": parent_sibling.get_abilities(user), - "created_at": parent_sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent_sibling.creator.id), - "depth": 3, - "excerpt": parent_sibling.excerpt, - "id": str(parent_sibling.id), - "is_favorite": False, - "link_reach": parent_sibling.link_reach, - "link_role": parent_sibling.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": parent_sibling.path, - "title": parent_sibling.title, - "updated_at": parent_sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - ] + assert response.json() == { + "abilities": grand_parent.get_abilities(user), + "children": [ + { + "abilities": parent.get_abilities(user), + "children": [ + { + "abilities": document.get_abilities(user), + "children": [ + { + "abilities": child.get_abilities(user), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 5, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [access.role], + }, + ], + "created_at": document.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(document.creator.id), + "depth": 4, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [access.role], + }, + { + "abilities": document_sibling.get_abilities(user), + "children": [], + "created_at": document_sibling.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(document_sibling.creator.id), + "depth": 4, + "excerpt": document_sibling.excerpt, + "id": str(document_sibling.id), + "is_favorite": False, + "link_reach": document_sibling.link_reach, + "link_role": document_sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": document_sibling.path, + "title": document_sibling.title, + "updated_at": document_sibling.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [access.role], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 3, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + }, + { + "abilities": parent_sibling.get_abilities(user), + "children": [], + "created_at": parent_sibling.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(parent_sibling.creator.id), + "depth": 3, + "excerpt": parent_sibling.excerpt, + "id": str(parent_sibling.id), + "is_favorite": False, + "link_reach": parent_sibling.link_reach, + "link_role": parent_sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": parent_sibling.path, + "title": parent_sibling.title, + "updated_at": parent_sibling.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [access.role], + }, + ], + "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(grand_parent.creator.id), + "depth": 2, + "excerpt": grand_parent.excerpt, + "id": str(grand_parent.id), + "is_favorite": False, + "link_reach": grand_parent.link_reach, + "link_role": grand_parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 2, + "path": grand_parent.path, + "title": grand_parent.title, + "updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + } def test_api_documents_tree_list_authenticated_related_team_none(mock_user_teams): @@ -804,73 +945,85 @@ def test_api_documents_tree_list_authenticated_related_team_members( # pylint: disable=R0801 assert response.status_code == 200 - assert response.json() == [ - { - "abilities": parent.get_abilities(user), - "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(parent.creator.id), - "depth": 1, - "excerpt": parent.excerpt, - "id": str(parent.id), - "is_favorite": False, - "link_reach": parent.link_reach, - "link_role": parent.link_role, - "numchild": 2, - "nb_accesses": 2, - "path": parent.path, - "title": parent.title, - "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": document.get_abilities(user), - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "depth": 2, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 1, - "nb_accesses": 2, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": child.get_abilities(user), - "created_at": child.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(child.creator.id), - "depth": 3, - "excerpt": child.excerpt, - "id": str(child.id), - "is_favorite": False, - "link_reach": child.link_reach, - "link_role": child.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": child.path, - "title": child.title, - "updated_at": child.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - { - "abilities": sibling.get_abilities(user), - "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(sibling.creator.id), - "depth": 2, - "excerpt": sibling.excerpt, - "id": str(sibling.id), - "is_favorite": False, - "link_reach": sibling.link_reach, - "link_role": sibling.link_role, - "numchild": 0, - "nb_accesses": 2, - "path": sibling.path, - "title": sibling.title, - "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), - "user_roles": [access.role], - }, - ] + assert response.json() == { + "abilities": parent.get_abilities(user), + "children": [ + { + "abilities": document.get_abilities(user), + "children": [ + { + "abilities": child.get_abilities(user), + "children": [], + "created_at": child.created_at.isoformat().replace( + "+00:00", "Z" + ), + "creator": str(child.creator.id), + "depth": 3, + "excerpt": child.excerpt, + "id": str(child.id), + "is_favorite": False, + "link_reach": child.link_reach, + "link_role": child.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": child.path, + "title": child.title, + "updated_at": child.updated_at.isoformat().replace( + "+00:00", "Z" + ), + "user_roles": [access.role], + }, + ], + "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), + "depth": 2, + "excerpt": document.excerpt, + "id": str(document.id), + "is_favorite": False, + "link_reach": document.link_reach, + "link_role": document.link_role, + "numchild": 1, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": document.path, + "title": document.title, + "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + }, + { + "abilities": sibling.get_abilities(user), + "children": [], + "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(sibling.creator.id), + "depth": 2, + "excerpt": sibling.excerpt, + "id": str(sibling.id), + "is_favorite": False, + "link_reach": sibling.link_reach, + "link_role": sibling.link_role, + "numchild": 0, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 0, + "path": sibling.path, + "title": sibling.title, + "updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + }, + ], + "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(parent.creator.id), + "depth": 1, + "excerpt": parent.excerpt, + "id": str(parent.id), + "is_favorite": False, + "link_reach": parent.link_reach, + "link_role": parent.link_role, + "numchild": 2, + "nb_accesses_ancestors": 2, + "nb_accesses_direct": 2, + "path": parent.path, + "title": parent.title, + "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [access.role], + } diff --git a/src/backend/core/tests/test_api_utils_nest_tree.py b/src/backend/core/tests/test_api_utils_nest_tree.py new file mode 100644 index 00000000..11d2d2f0 --- /dev/null +++ b/src/backend/core/tests/test_api_utils_nest_tree.py @@ -0,0 +1,107 @@ +"""Unit tests for the nest_tree utility function.""" + +import pytest + +from core.api.utils import nest_tree + + +def test_api_utils_nest_tree_empty_list(): + """Test that an empty list returns an empty nested structure.""" + # pylint: disable=use-implicit-booleaness-not-comparison + assert nest_tree([], 4) is None + + +def test_api_utils_nest_tree_single_document(): + """Test that a single document is returned as the only root element.""" + documents = [{"id": "1", "path": "0001"}] + expected = {"id": "1", "path": "0001", "children": []} + assert nest_tree(documents, 4) == expected + + +def test_api_utils_nest_tree_multiple_root_documents(): + """Test that multiple root-level documents are correctly added to the root.""" + documents = [ + {"id": "1", "path": "0001"}, + {"id": "2", "path": "0002"}, + ] + with pytest.raises( + ValueError, + match="More than one root element detected.", + ): + nest_tree(documents, 4) + + +def test_api_utils_nest_tree_nested_structure(): + """Test that documents are correctly nested based on path levels.""" + documents = [ + {"id": "1", "path": "0001"}, + {"id": "2", "path": "00010001"}, + {"id": "3", "path": "000100010001"}, + {"id": "4", "path": "00010002"}, + ] + expected = { + "id": "1", + "path": "0001", + "children": [ + { + "id": "2", + "path": "00010001", + "children": [{"id": "3", "path": "000100010001", "children": []}], + }, + {"id": "4", "path": "00010002", "children": []}, + ], + } + assert nest_tree(documents, 4) == expected + + +def test_api_utils_nest_tree_siblings_at_same_path(): + """ + Test that sibling documents with the same path are correctly grouped under the same parent. + """ + documents = [ + {"id": "1", "path": "0001"}, + {"id": "2", "path": "00010001"}, + {"id": "3", "path": "00010002"}, + ] + expected = { + "id": "1", + "path": "0001", + "children": [ + {"id": "2", "path": "00010001", "children": []}, + {"id": "3", "path": "00010002", "children": []}, + ], + } + assert nest_tree(documents, 4) == expected + + +def test_api_utils_nest_tree_decreasing_path_resets_parent(): + """Test that a document at a lower path resets the parent assignment correctly.""" + documents = [ + {"id": "1", "path": "0001"}, + {"id": "6", "path": "00010001"}, + {"id": "2", "path": "00010002"}, # unordered + {"id": "5", "path": "000100010001"}, + {"id": "3", "path": "000100010002"}, + {"id": "4", "path": "00010003"}, + ] + expected = { + "id": "1", + "path": "0001", + "children": [ + { + "id": "6", + "path": "00010001", + "children": [ + {"id": "5", "path": "000100010001", "children": []}, + {"id": "3", "path": "000100010002", "children": []}, + ], + }, + { + "id": "2", + "path": "00010002", + "children": [], + }, + {"id": "4", "path": "00010003", "children": []}, + ], + } + assert nest_tree(documents, 4) == expected