✨(backend) add new "descendants" action to document API endpoint
We want to be able to make a search query inside a hierchical document. It's elegant to do it as a document detail action so that we benefit from access control.
This commit is contained in:
committed by
Manuel Raynaud
parent
56aa69f56a
commit
2203d49a52
@@ -34,6 +34,7 @@ and this project adheres to
|
|||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
|
- ✨(backend) add new "descendants" action to document API endpoint #645
|
||||||
- ✨(backend) new "tree" action on document detail endpoint #645
|
- ✨(backend) new "tree" action on document detail endpoint #645
|
||||||
- ✨(backend) allow forcing page size within limits #645
|
- ✨(backend) allow forcing page size within limits #645
|
||||||
- 💄(frontend) add error pages #643
|
- 💄(frontend) add error pages #643
|
||||||
|
|||||||
@@ -177,7 +177,14 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
|||||||
request = self.context.get("request")
|
request = self.context.get("request")
|
||||||
|
|
||||||
if request:
|
if request:
|
||||||
return document.get_abilities(request.user)
|
paths_links_mapping = self.context.get("paths_links_mapping", None)
|
||||||
|
# Retrieve ancestor links from paths_links_mapping (if provided)
|
||||||
|
ancestors_links = (
|
||||||
|
paths_links_mapping.get(document.path[: -document.steplen])
|
||||||
|
if paths_links_mapping
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return document.get_abilities(request.user, ancestors_links=ancestors_links)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -21,7 +20,6 @@ from django.http import Http404
|
|||||||
|
|
||||||
import rest_framework as drf
|
import rest_framework as drf
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
from django_filters import rest_framework as drf_filters
|
|
||||||
from rest_framework import filters, status, viewsets
|
from rest_framework import filters, status, viewsets
|
||||||
from rest_framework import response as drf_response
|
from rest_framework import response as drf_response
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
@@ -424,6 +422,7 @@ class DocumentViewSet(
|
|||||||
serializer_class = serializers.DocumentSerializer
|
serializer_class = serializers.DocumentSerializer
|
||||||
ai_translate_serializer_class = serializers.AITranslateSerializer
|
ai_translate_serializer_class = serializers.AITranslateSerializer
|
||||||
children_serializer_class = serializers.ListDocumentSerializer
|
children_serializer_class = serializers.ListDocumentSerializer
|
||||||
|
descendants_serializer_class = serializers.ListDocumentSerializer
|
||||||
list_serializer_class = serializers.ListDocumentSerializer
|
list_serializer_class = serializers.ListDocumentSerializer
|
||||||
trashbin_serializer_class = serializers.ListDocumentSerializer
|
trashbin_serializer_class = serializers.ListDocumentSerializer
|
||||||
tree_serializer_class = serializers.ListDocumentSerializer
|
tree_serializer_class = serializers.ListDocumentSerializer
|
||||||
@@ -841,17 +840,24 @@ class DocumentViewSet(
|
|||||||
else drf.exceptions.NotAuthenticated()
|
else drf.exceptions.NotAuthenticated()
|
||||||
)
|
)
|
||||||
|
|
||||||
ancestors_links_definitions = defaultdict(set)
|
paths_links_mapping = {}
|
||||||
|
ancestors_links = []
|
||||||
children_clause = db.Q()
|
children_clause = db.Q()
|
||||||
for ancestor in ancestors:
|
for ancestor in ancestors:
|
||||||
if ancestor.depth < highest_readable.depth:
|
if ancestor.depth < highest_readable.depth:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ancestors_links_definitions[ancestor.link_reach].add(ancestor.link_role)
|
|
||||||
children_clause |= db.Q(
|
children_clause |= db.Q(
|
||||||
path__startswith=ancestor.path, depth=ancestor.depth + 1
|
path__startswith=ancestor.path, depth=ancestor.depth + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Compute cache for ancestors links to avoid many queries while computing
|
||||||
|
# abilties for his documents in the tree!
|
||||||
|
ancestors_links.append(
|
||||||
|
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
|
||||||
|
)
|
||||||
|
paths_links_mapping[ancestor.path] = ancestors_links.copy()
|
||||||
|
|
||||||
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
|
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
|
||||||
|
|
||||||
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
|
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
|
||||||
@@ -866,7 +872,7 @@ class DocumentViewSet(
|
|||||||
many=True,
|
many=True,
|
||||||
context={
|
context={
|
||||||
"request": request,
|
"request": request,
|
||||||
"ancestors_links_definitions": ancestors_links_definitions,
|
"paths_links_mapping": paths_links_mapping,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return drf.response.Response(
|
return drf.response.Response(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Declare and configure the models for the impress core application
|
|||||||
import hashlib
|
import hashlib
|
||||||
import smtplib
|
import smtplib
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
@@ -649,22 +650,27 @@ class Document(MP_Node, BaseModel):
|
|||||||
roles = []
|
roles = []
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
@cached_property
|
def get_links_definitions(self, ancestors_links=None):
|
||||||
def links_definitions(self):
|
|
||||||
"""Get links reach/role definitions for the current document and its ancestors."""
|
"""Get links reach/role definitions for the current document and its ancestors."""
|
||||||
links_definitions = {self.link_reach: {self.link_role}}
|
|
||||||
|
|
||||||
# Ancestors links definitions are only interesting if the document is not the highest
|
links_definitions = defaultdict(set)
|
||||||
# ancestor to which the current user has access. Look for the annotation:
|
links_definitions[self.link_reach].add(self.link_role)
|
||||||
if self.depth > 1 and not getattr(self, "is_highest_ancestor_for_user", False):
|
|
||||||
for ancestor in self.get_ancestors().values("link_reach", "link_role"):
|
|
||||||
links_definitions.setdefault(ancestor["link_reach"], set()).add(
|
|
||||||
ancestor["link_role"]
|
|
||||||
)
|
|
||||||
|
|
||||||
return links_definitions
|
# Skip ancestor processing if the document is the highest accessible ancestor
|
||||||
|
if self.depth <= 1 or getattr(self, "is_highest_ancestor_for_user", False):
|
||||||
|
return links_definitions
|
||||||
|
|
||||||
def get_abilities(self, user):
|
# Fallback to querying the DB if ancestors links are not provided
|
||||||
|
if ancestors_links is None:
|
||||||
|
ancestors_links = self.get_ancestors().values("link_reach", "link_role")
|
||||||
|
|
||||||
|
# Merge ancestor link definitions
|
||||||
|
for ancestor in ancestors_links:
|
||||||
|
links_definitions[ancestor["link_reach"]].add(ancestor["link_role"])
|
||||||
|
|
||||||
|
return dict(links_definitions) # Convert defaultdict back to a normal dict
|
||||||
|
|
||||||
|
def get_abilities(self, user, ancestors_links=None):
|
||||||
"""
|
"""
|
||||||
Compute and return abilities for a given user on the document.
|
Compute and return abilities for a given user on the document.
|
||||||
"""
|
"""
|
||||||
@@ -689,7 +695,7 @@ class Document(MP_Node, BaseModel):
|
|||||||
# Add roles provided by the document link, taking into account its ancestors
|
# Add roles provided by the document link, taking into account its ancestors
|
||||||
|
|
||||||
# Add roles provided by the document link
|
# Add roles provided by the document link
|
||||||
links_definitions = self.links_definitions
|
links_definitions = self.get_links_definitions(ancestors_links=ancestors_links)
|
||||||
public_roles = links_definitions.get(LinkReachChoices.PUBLIC, set())
|
public_roles = links_definitions.get(LinkReachChoices.PUBLIC, set())
|
||||||
authenticated_roles = (
|
authenticated_roles = (
|
||||||
links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
|
links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
|
||||||
@@ -724,6 +730,7 @@ class Document(MP_Node, BaseModel):
|
|||||||
"children_list": can_get,
|
"children_list": can_get,
|
||||||
"children_create": can_update and user.is_authenticated,
|
"children_create": can_update and user.is_authenticated,
|
||||||
"collaboration_auth": can_get,
|
"collaboration_auth": can_get,
|
||||||
|
"descendants": can_get,
|
||||||
"destroy": is_owner,
|
"destroy": is_owner,
|
||||||
"favorite": can_get and user.is_authenticated,
|
"favorite": can_get and user.is_authenticated,
|
||||||
"link_configuration": is_owner_or_admin,
|
"link_configuration": is_owner_or_admin,
|
||||||
|
|||||||
@@ -0,0 +1,676 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: descendants
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||||
|
"""Anonymous users should be allowed to retrieve the descendants of a public document."""
|
||||||
|
document = factories.DocumentFactory(link_reach="public")
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/descendants/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(AnonymousUser()),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(AnonymousUser()),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 3,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(AnonymousUser()),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 0,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_anonymous_public_parent():
|
||||||
|
"""
|
||||||
|
Anonymous users should be allowed to retrieve the descendants of a document who
|
||||||
|
has a public ancestor.
|
||||||
|
"""
|
||||||
|
grand_parent = factories.DocumentFactory(link_reach="public")
|
||||||
|
parent = factories.DocumentFactory(
|
||||||
|
parent=grand_parent, link_reach=random.choice(["authenticated", "restricted"])
|
||||||
|
)
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach=random.choice(["authenticated", "restricted"]), parent=parent
|
||||||
|
)
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/descendants/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(AnonymousUser()),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 4,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(AnonymousUser()),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 5,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(AnonymousUser()),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 4,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 0,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reach", ["restricted", "authenticated"])
|
||||||
|
def test_api_documents_descendants_list_anonymous_restricted_or_authenticated(reach):
|
||||||
|
"""
|
||||||
|
Anonymous users should not be able to retrieve descendants of a document that is not public.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
child = factories.DocumentFactory(parent=document)
|
||||||
|
_grand_child = factories.DocumentFactory(parent=child)
|
||||||
|
|
||||||
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/descendants/")
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||||
|
def test_api_documents_descendants_list_authenticated_unrelated_public_or_authenticated(
|
||||||
|
reach,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to retrieve the descendants of a public/authenticated
|
||||||
|
document to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(user),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(user),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 3,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(user),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 0,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||||
|
def test_api_documents_descendants_list_authenticated_public_or_authenticated_parent(
|
||||||
|
reach,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve the descendants of a document who
|
||||||
|
has a public or authenticated ancestor.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
grand_parent = factories.DocumentFactory(link_reach=reach)
|
||||||
|
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted", parent=parent)
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/descendants/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(user),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 4,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(user),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 5,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(user),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 4,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 0,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_authenticated_unrelated_restricted():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to retrieve the descendants of a document that is
|
||||||
|
restricted and to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
child1, _child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
_grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_authenticated_related_direct():
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve the descendants of a document
|
||||||
|
to which they are directly related whatever the role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document, user=user)
|
||||||
|
factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(user),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 3,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [access.role],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(user),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 3,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 3,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [access.role],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(user),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 2,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [access.role],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_authenticated_related_parent():
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve the descendants of a document if they
|
||||||
|
are related to one of its ancestors whatever the role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
grand_parent = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
grand_parent_access = factories.UserDocumentAccessFactory(
|
||||||
|
document=grand_parent, user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
|
||||||
|
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
|
||||||
|
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
factories.UserDocumentAccessFactory(document=child1)
|
||||||
|
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(user),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 4,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 2,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [grand_parent_access.role],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(user),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 5,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 2,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [grand_parent_access.role],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(user),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 4,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [grand_parent_access.role],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_authenticated_related_child():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to retrieve all the descendants of a document
|
||||||
|
as a result of being related to one of its children.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
child1, _child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
_grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
factories.UserDocumentAccessFactory(document=child1, user=user)
|
||||||
|
factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_authenticated_related_team_none(
|
||||||
|
mock_user_teams,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to retrieve the descendants of a restricted document
|
||||||
|
related to teams in which the user is not.
|
||||||
|
"""
|
||||||
|
mock_user_teams.return_value = []
|
||||||
|
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
|
||||||
|
factories.TeamDocumentAccessFactory(document=document, team="myteam")
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/descendants/")
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||||
|
mock_user_teams,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve the descendants of a document to which they
|
||||||
|
are related via a team whatever the role.
|
||||||
|
"""
|
||||||
|
mock_user_teams.return_value = ["myteam"]
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
grand_child = factories.DocumentFactory(parent=child1)
|
||||||
|
|
||||||
|
access = factories.TeamDocumentAccessFactory(document=document, team="myteam")
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/descendants/")
|
||||||
|
|
||||||
|
# pylint: disable=R0801
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 3,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"abilities": child1.get_abilities(user),
|
||||||
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child1.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child1.excerpt,
|
||||||
|
"id": str(child1.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child1.link_reach,
|
||||||
|
"link_role": child1.link_role,
|
||||||
|
"numchild": 1,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child1.path,
|
||||||
|
"title": child1.title,
|
||||||
|
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [access.role],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": grand_child.get_abilities(user),
|
||||||
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(grand_child.creator.id),
|
||||||
|
"depth": 3,
|
||||||
|
"excerpt": grand_child.excerpt,
|
||||||
|
"id": str(grand_child.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": grand_child.link_reach,
|
||||||
|
"link_role": grand_child.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": grand_child.path,
|
||||||
|
"title": grand_child.title,
|
||||||
|
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [access.role],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abilities": child2.get_abilities(user),
|
||||||
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"creator": str(child2.creator.id),
|
||||||
|
"depth": 2,
|
||||||
|
"excerpt": child2.excerpt,
|
||||||
|
"id": str(child2.id),
|
||||||
|
"is_favorite": False,
|
||||||
|
"link_reach": child2.link_reach,
|
||||||
|
"link_role": child2.link_role,
|
||||||
|
"numchild": 0,
|
||||||
|
"nb_accesses": 1,
|
||||||
|
"path": child2.path,
|
||||||
|
"title": child2.title,
|
||||||
|
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user_roles": [access.role],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: list
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from faker import Faker
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
|
||||||
|
fake = Faker()
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
# Filters: unknown field
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_descendants_filter_unknown_field():
|
||||||
|
"""
|
||||||
|
Trying to filter by an unknown field should be ignored.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
factories.DocumentFactory()
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[user])
|
||||||
|
expected_ids = {
|
||||||
|
str(document.id)
|
||||||
|
for document in factories.DocumentFactory.create_batch(2, parent=document)
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/?unknown=true"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 2
|
||||||
|
assert {result["id"] for result in results} == expected_ids
|
||||||
|
|
||||||
|
|
||||||
|
# Filters: title
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"query,nb_results",
|
||||||
|
[
|
||||||
|
("Project Alpha", 1), # Exact match
|
||||||
|
("project", 2), # Partial match (case-insensitive)
|
||||||
|
("Guide", 1), # Word match within a title
|
||||||
|
("Special", 0), # No match (nonexistent keyword)
|
||||||
|
("2024", 2), # Match by numeric keyword
|
||||||
|
("", 5), # Empty string
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_descendants_filter_title(query, nb_results):
|
||||||
|
"""Authenticated users should be able to search documents by their title."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[user])
|
||||||
|
|
||||||
|
# Create documents with predefined titles
|
||||||
|
titles = [
|
||||||
|
"Project Alpha Documentation",
|
||||||
|
"Project Beta Overview",
|
||||||
|
"User Guide",
|
||||||
|
"Financial Report 2024",
|
||||||
|
"Annual Review 2024",
|
||||||
|
]
|
||||||
|
for title in titles:
|
||||||
|
factories.DocumentFactory(title=title, parent=document)
|
||||||
|
|
||||||
|
# Perform the search query
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/descendants/?title={query:s}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == nb_results
|
||||||
|
|
||||||
|
# Ensure all results contain the query in their title
|
||||||
|
for result in results:
|
||||||
|
assert query.lower().strip() in result["title"].lower()
|
||||||
@@ -34,6 +34,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
# Anonymous user can't favorite a document even with read access
|
# Anonymous user can't favorite a document even with read access
|
||||||
"favorite": False,
|
"favorite": False,
|
||||||
@@ -91,6 +92,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
# Anonymous user can't favorite a document even with read access
|
# Anonymous user can't favorite a document even with read access
|
||||||
"favorite": False,
|
"favorite": False,
|
||||||
@@ -182,6 +184,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"children_create": document.link_role == "editor",
|
"children_create": document.link_role == "editor",
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -246,6 +249,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
|||||||
"children_create": grand_parent.link_role == "editor",
|
"children_create": grand_parent.link_role == "editor",
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -419,6 +423,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
|||||||
"children_create": access.role != "reader",
|
"children_create": access.role != "reader",
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": access.role == "owner",
|
"destroy": access.role == "owner",
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": access.role == "owner",
|
"invite_owner": access.role == "owner",
|
||||||
@@ -724,7 +729,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_user_roles(django_assert_num_queries):
|
def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
|
||||||
"""
|
"""
|
||||||
Roles should be annotated on querysets taking into account all documents ancestors.
|
Roles should be annotated on querysets taking into account all documents ancestors.
|
||||||
"""
|
"""
|
||||||
@@ -749,7 +754,7 @@ def test_api_documents_retrieve_user_roles(django_assert_num_queries):
|
|||||||
)
|
)
|
||||||
expected_roles = {access.role for access in accesses}
|
expected_roles = {access.role for access in accesses}
|
||||||
|
|
||||||
with django_assert_num_queries(10):
|
with django_assert_max_num_queries(11):
|
||||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ def test_api_documents_trashbin_format():
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": True,
|
"destroy": True,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": True,
|
"invite_owner": True,
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ pytestmark = pytest.mark.django_db
|
|||||||
def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_queries):
|
def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_queries):
|
||||||
"""Anonymous users should be allowed to retrieve the tree of a public document."""
|
"""Anonymous users should be allowed to retrieve the tree of a public document."""
|
||||||
parent = factories.DocumentFactory(link_reach="public")
|
parent = factories.DocumentFactory(link_reach="public")
|
||||||
document, sibling = factories.DocumentFactory.create_batch(2, parent=parent)
|
document, sibling1, sibling2 = factories.DocumentFactory.create_batch(
|
||||||
|
3, parent=parent
|
||||||
|
)
|
||||||
child = factories.DocumentFactory(link_reach="public", parent=document)
|
child = factories.DocumentFactory(link_reach="public", parent=document)
|
||||||
|
|
||||||
with django_assert_num_queries(8):
|
with django_assert_num_queries(9):
|
||||||
APIClient().get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
APIClient().get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||||
|
|
||||||
with django_assert_num_queries(4):
|
with django_assert_num_queries(4):
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ def test_models_documents_get_abilities_forbidden(
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": False,
|
"children_list": False,
|
||||||
"collaboration_auth": False,
|
"collaboration_auth": False,
|
||||||
|
"descendants": False,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": False,
|
"favorite": False,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -209,6 +210,7 @@ def test_models_documents_get_abilities_reader(
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": is_authenticated,
|
"favorite": is_authenticated,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -258,6 +260,7 @@ def test_models_documents_get_abilities_editor(
|
|||||||
"children_create": is_authenticated,
|
"children_create": is_authenticated,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": is_authenticated,
|
"favorite": is_authenticated,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -297,6 +300,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": True,
|
"destroy": True,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": True,
|
"invite_owner": True,
|
||||||
@@ -337,6 +341,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -376,6 +381,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -422,6 +428,7 @@ def test_models_documents_get_abilities_reader_user(
|
|||||||
"children_create": access_from_link,
|
"children_create": access_from_link,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
@@ -466,6 +473,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"descendants": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
|||||||
Reference in New Issue
Block a user