Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3244c347d | |||
| b193a6e72b | |||
| 1c501aaca4 | |||
| c24066d3f7 | |||
| 2966323cb0 | |||
| 21ef3cda00 | |||
| ca0ac7e79c | |||
| 5021eac019 | |||
| 619ef9050c | |||
| 68b8e52ab0 | |||
| 859f4ade9c | |||
| fd424a0372 | |||
| 70328a721b | |||
| 42197bc31a | |||
| 561398eb66 | |||
| 1f7a3d8501 | |||
| 2baba1182d | |||
| a17332cace | |||
| 5008916a58 | |||
| c717f7af3b | |||
| a5cb1056de | |||
| 402d007295 | |||
| 7347329e8d |
@@ -85,6 +85,8 @@ and this project adheres to
|
||||
|
||||
## Added
|
||||
|
||||
- ✨(backend) include ancestors accesses on document accesses list view #846
|
||||
- ✨(backend) add ancestors links reach and role to document API #846
|
||||
- 🚸(backend) make document search on title accent-insensitive #874
|
||||
- 🚩 add homepage feature flag #861
|
||||
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
|
||||
@@ -96,12 +98,15 @@ and this project adheres to
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️(backend) stop requiring owner for non-root documents #846
|
||||
- ♻️(backend) simplify roles by ranking them and return only the max role #846
|
||||
- ⚡️(frontend) reduce unblocking time for config #867
|
||||
- ♻️(frontend) bind UI with ability access #900
|
||||
- ♻️(frontend) use built-in Quote block #908
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) fix link definition select options linked to ancestors #846
|
||||
- 🐛(nginx) fix 404 when accessing a doc #866
|
||||
- 🔒️(drf) disable browsable HTML API renderer #919
|
||||
- 🔒(frontend) enhance file download security #889
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Permission handlers for the impress core app."""
|
||||
|
||||
from django.core import exceptions
|
||||
from django.db.models import Q
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
from core.models import DocumentAccess, RoleChoices, get_trashbin_cutoff
|
||||
from core import choices
|
||||
from core.models import RoleChoices, get_trashbin_cutoff
|
||||
|
||||
ACTION_FOR_METHOD_TO_PERMISSION = {
|
||||
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"},
|
||||
@@ -80,42 +80,37 @@ class CanCreateInvitationPermission(permissions.BasePermission):
|
||||
if view.action != "create":
|
||||
return True
|
||||
|
||||
# Check if resource_id is passed in the context
|
||||
try:
|
||||
document_id = view.kwargs["resource_id"]
|
||||
except KeyError as exc:
|
||||
raise exceptions.ValidationError(
|
||||
"You must set a document ID in kwargs to manage document invitations."
|
||||
) from exc
|
||||
|
||||
# Check if the user has access to manage invitations (Owner/Admin roles)
|
||||
return DocumentAccess.objects.filter(
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
document=document_id,
|
||||
role__in=[RoleChoices.OWNER, RoleChoices.ADMIN],
|
||||
).exists()
|
||||
role = view.document.get_role(user)
|
||||
if role not in choices.PRIVILEGED_ROLES:
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to create invitations for this document."
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AccessPermission(permissions.BasePermission):
|
||||
"""Permission class for access objects."""
|
||||
class ResourceWithAccessPermission(permissions.BasePermission):
|
||||
"""A permission class for templates and invitations."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""check create permission for templates."""
|
||||
return request.user.is_authenticated or view.action != "create"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Check permission for a given object."""
|
||||
abilities = obj.get_abilities(request.user)
|
||||
action = view.action
|
||||
try:
|
||||
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
|
||||
except KeyError:
|
||||
pass
|
||||
return abilities.get(action, False)
|
||||
|
||||
|
||||
class DocumentAccessPermission(AccessPermission):
|
||||
class DocumentPermission(permissions.BasePermission):
|
||||
"""Subclass to handle soft deletion specificities."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""check create permission for documents."""
|
||||
return request.user.is_authenticated or view.action != "create"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""
|
||||
Return a 404 on deleted documents
|
||||
@@ -127,10 +122,45 @@ class DocumentAccessPermission(AccessPermission):
|
||||
) and deleted_at < get_trashbin_cutoff():
|
||||
raise Http404
|
||||
|
||||
# Compute permission first to ensure the "user_roles" attribute is set
|
||||
has_permission = super().has_object_permission(request, view, obj)
|
||||
abilities = obj.get_abilities(request.user)
|
||||
action = view.action
|
||||
try:
|
||||
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
has_permission = abilities.get(action, False)
|
||||
|
||||
if obj.ancestors_deleted_at and not RoleChoices.OWNER in obj.user_roles:
|
||||
raise Http404
|
||||
|
||||
return has_permission
|
||||
|
||||
|
||||
class ResourceAccessPermission(IsAuthenticated):
|
||||
"""Permission class for document access objects."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""check create permission for accesses in documents tree."""
|
||||
if super().has_permission(request, view) is False:
|
||||
return False
|
||||
|
||||
if view.action == "create":
|
||||
role = getattr(view, view.resource_field_name).get_role(request.user)
|
||||
if role not in choices.PRIVILEGED_ROLES:
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage accesses for this resource."
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Check permission for a given object."""
|
||||
abilities = obj.get_abilities(request.user)
|
||||
|
||||
requested_role = request.data.get("role")
|
||||
if requested_role and requested_role not in abilities.get("set_role_to", []):
|
||||
return False
|
||||
|
||||
action = view.action
|
||||
return abilities.get(action, False)
|
||||
|
||||
+201
-143
@@ -5,14 +5,13 @@ import mimetypes
|
||||
from base64 import b64decode
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import magic
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework import serializers
|
||||
|
||||
from core import enums, models, utils
|
||||
from core import choices, enums, models, utils
|
||||
from core.services.ai_services import AI_ACTIONS
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
@@ -32,134 +31,35 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
class UserLightSerializer(UserSerializer):
|
||||
"""Serialize users with limited fields."""
|
||||
|
||||
id = serializers.SerializerMethodField(read_only=True)
|
||||
email = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_id(self, _user):
|
||||
"""Return always None. Here to have the same fields than in UserSerializer."""
|
||||
return None
|
||||
|
||||
def get_email(self, _user):
|
||||
"""Return always None. Here to have the same fields than in UserSerializer."""
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "email", "full_name", "short_name"]
|
||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||
fields = ["full_name", "short_name"]
|
||||
read_only_fields = ["full_name", "short_name"]
|
||||
|
||||
|
||||
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
class TemplateAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize template accesses."""
|
||||
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Make "user" field is readonly but only on update."""
|
||||
validated_data.pop("user", None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def get_abilities(self, access) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return access.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Check access rights specific to writing (create/update)
|
||||
"""
|
||||
request = self.context.get("request")
|
||||
user = getattr(request, "user", None)
|
||||
role = attrs.get("role")
|
||||
|
||||
# Update
|
||||
if self.instance:
|
||||
can_set_role_to = self.instance.get_abilities(user)["set_role_to"]
|
||||
|
||||
if role and role not in can_set_role_to:
|
||||
message = (
|
||||
f"You are only allowed to set role to {', '.join(can_set_role_to)}"
|
||||
if can_set_role_to
|
||||
else "You are not allowed to set this role for this template."
|
||||
)
|
||||
raise exceptions.PermissionDenied(message)
|
||||
|
||||
# Create
|
||||
else:
|
||||
try:
|
||||
resource_id = self.context["resource_id"]
|
||||
except KeyError as exc:
|
||||
raise exceptions.ValidationError(
|
||||
"You must set a resource ID in kwargs to create a new access."
|
||||
) from exc
|
||||
|
||||
if not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
||||
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
||||
).exists():
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage accesses for this resource."
|
||||
)
|
||||
|
||||
if (
|
||||
role == models.RoleChoices.OWNER
|
||||
and not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
role=models.RoleChoices.OWNER,
|
||||
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
||||
).exists()
|
||||
):
|
||||
raise exceptions.PermissionDenied(
|
||||
"Only owners of a resource can assign other users as owners."
|
||||
)
|
||||
|
||||
# pylint: disable=no-member
|
||||
attrs[f"{self.Meta.resource_field_name}_id"] = self.context["resource_id"]
|
||||
return attrs
|
||||
|
||||
|
||||
class DocumentAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize document accesses."""
|
||||
|
||||
user_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.User.objects.all(),
|
||||
write_only=True,
|
||||
source="user",
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
user = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = ["id", "user", "user_id", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
|
||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||
"""Serialize document accesses with limited fields."""
|
||||
|
||||
user = UserLightSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
fields = ["id", "user", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "team", "role", "abilities"]
|
||||
|
||||
|
||||
class TemplateAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize template accesses."""
|
||||
|
||||
class Meta:
|
||||
model = models.TemplateAccess
|
||||
resource_field_name = "template"
|
||||
fields = ["id", "user", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
def get_abilities(self, instance) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return instance.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Make "user" field is readonly but only on update."""
|
||||
validated_data.pop("user", None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"""Serialize documents with limited fields for display in lists."""
|
||||
@@ -167,7 +67,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
nb_accesses_ancestors = serializers.IntegerField(read_only=True)
|
||||
nb_accesses_direct = serializers.IntegerField(read_only=True)
|
||||
user_roles = serializers.SerializerMethodField(read_only=True)
|
||||
user_role = serializers.SerializerMethodField(read_only=True)
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -175,6 +75,10 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -188,11 +92,15 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"path",
|
||||
"title",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -205,34 +113,45 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"numchild",
|
||||
"path",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
|
||||
def get_abilities(self, document) -> dict:
|
||||
def to_representation(self, instance):
|
||||
"""Precompute once per instance"""
|
||||
paths_links_mapping = self.context.get("paths_links_mapping")
|
||||
|
||||
if paths_links_mapping is not None:
|
||||
links = paths_links_mapping.get(instance.path[: -instance.steplen], [])
|
||||
instance.ancestors_link_definition = choices.get_equivalent_link_definition(
|
||||
links
|
||||
)
|
||||
|
||||
return super().to_representation(instance)
|
||||
|
||||
def get_abilities(self, instance) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if not request:
|
||||
return {}
|
||||
|
||||
if request:
|
||||
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 instance.get_abilities(request.user)
|
||||
|
||||
return {}
|
||||
|
||||
def get_user_roles(self, document):
|
||||
def get_user_role(self, instance):
|
||||
"""
|
||||
Return roles of the logged-in user for the current document,
|
||||
taking into account ancestors.
|
||||
"""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return document.get_roles(request.user)
|
||||
return []
|
||||
return instance.get_role(request.user) if request else None
|
||||
|
||||
|
||||
class DocumentLightSerializer(serializers.ModelSerializer):
|
||||
"""Minial document serializer for nesting in document accesses."""
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
fields = ["id", "path", "depth"]
|
||||
read_only_fields = ["id", "path", "depth"]
|
||||
|
||||
|
||||
class DocumentSerializer(ListDocumentSerializer):
|
||||
@@ -245,6 +164,10 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"content",
|
||||
"created_at",
|
||||
"creator",
|
||||
@@ -259,11 +182,15 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"path",
|
||||
"title",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -275,7 +202,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"numchild",
|
||||
"path",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
|
||||
def get_fields(self):
|
||||
@@ -361,6 +288,140 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
return super().save(**kwargs)
|
||||
|
||||
|
||||
class DocumentAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize document accesses."""
|
||||
|
||||
document = DocumentLightSerializer(read_only=True)
|
||||
user_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.User.objects.all(),
|
||||
write_only=True,
|
||||
source="user",
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
user = UserSerializer(read_only=True)
|
||||
team = serializers.CharField(required=False, allow_blank=True)
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
max_ancestors_role = serializers.SerializerMethodField(read_only=True)
|
||||
max_role = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = [
|
||||
"id",
|
||||
"document",
|
||||
"user",
|
||||
"user_id",
|
||||
"team",
|
||||
"role",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"document",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
|
||||
def get_abilities(self, instance) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return instance.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def get_max_ancestors_role(self, instance):
|
||||
"""Return max_ancestors_role if annotated; else None."""
|
||||
return getattr(instance, "max_ancestors_role", None)
|
||||
|
||||
def get_max_role(self, instance):
|
||||
"""Return max role."""
|
||||
return choices.RoleChoices.max(
|
||||
getattr(instance, "max_ancestors_role", None),
|
||||
instance.role,
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Ensure the selected role is greater than or equal to the maximum role
|
||||
found in any ancestor document for the same user/team.
|
||||
"""
|
||||
|
||||
if self.instance:
|
||||
document = self.instance.document
|
||||
user = self.instance.user
|
||||
team = self.instance.team
|
||||
else:
|
||||
document_id = self.context["resource_id"]
|
||||
document = models.Document.objects.get(id=document_id)
|
||||
team = attrs.get("team")
|
||||
user = attrs.get("user")
|
||||
|
||||
ancestors = document.get_ancestors().filter(ancestors_deleted_at__isnull=True)
|
||||
access_qs = models.DocumentAccess.objects.filter(document__in=ancestors)
|
||||
|
||||
if user:
|
||||
access_qs = access_qs.filter(user=user)
|
||||
if team:
|
||||
access_qs = access_qs.filter(team=team)
|
||||
|
||||
ancestor_roles = access_qs.values_list("role", flat=True)
|
||||
inherited_role = choices.RoleChoices.max(*ancestor_roles)
|
||||
|
||||
role = attrs.get("role")
|
||||
get_priority = choices.RoleChoices.get_priority
|
||||
if inherited_role and get_priority(inherited_role) >= get_priority(role):
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"role": (
|
||||
"Role overrides must be greater than the inherited role: "
|
||||
f"{inherited_role}/{role}"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Make "user" field readonly but only on update."""
|
||||
validated_data.pop("team", None)
|
||||
validated_data.pop("user", None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||
"""Serialize document accesses with limited fields."""
|
||||
|
||||
user = UserLightSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = [
|
||||
"id",
|
||||
"document",
|
||||
"user",
|
||||
"team",
|
||||
"role",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"document",
|
||||
"team",
|
||||
"role",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
|
||||
|
||||
class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for creating a document from a server-to-server request.
|
||||
@@ -653,11 +714,8 @@ class InvitationSerializer(serializers.ModelSerializer):
|
||||
|
||||
# If the role is OWNER, check if the user has OWNER access
|
||||
if role == models.RoleChoices.OWNER:
|
||||
if not models.DocumentAccess.objects.filter(
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
document=document_id,
|
||||
role=models.RoleChoices.OWNER,
|
||||
).exists():
|
||||
user_role = models.Document.objects.get(id=document_id).get_role(user)
|
||||
if user_role != models.RoleChoices.OWNER:
|
||||
raise serializers.ValidationError(
|
||||
"Only owners of a document can invite other users as owners."
|
||||
)
|
||||
|
||||
+270
-223
@@ -4,11 +4,11 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from urllib.parse import unquote, urlencode, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -19,6 +19,7 @@ from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Left, Length
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import capfirst, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -33,7 +34,7 @@ from rest_framework import response as drf_response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
from core import authentication, enums, models
|
||||
from core import authentication, choices, enums, models
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.tasks.mail import send_ask_for_access_mail
|
||||
@@ -223,14 +224,10 @@ class UserViewSet(
|
||||
class ResourceAccessViewsetMixin:
|
||||
"""Mixin with methods common to all access viewsets."""
|
||||
|
||||
def get_permissions(self):
|
||||
"""User only needs to be authenticated to list resource accesses"""
|
||||
if self.action == "list":
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
else:
|
||||
return super().get_permissions()
|
||||
|
||||
return [permission() for permission in permission_classes]
|
||||
def filter_queryset(self, queryset):
|
||||
"""Override to filter on related resource."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
return queryset.filter(**{self.resource_field_name: self.kwargs["resource_id"]})
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extra context provided to the serializer class."""
|
||||
@@ -238,80 +235,6 @@ class ResourceAccessViewsetMixin:
|
||||
context["resource_id"] = self.kwargs["resource_id"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(
|
||||
**{self.resource_field_name: self.kwargs["resource_id"]}
|
||||
)
|
||||
|
||||
if self.action == "list":
|
||||
user = self.request.user
|
||||
teams = user.teams
|
||||
user_roles_query = (
|
||||
queryset.filter(
|
||||
db.Q(user=user) | db.Q(team__in=teams),
|
||||
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||
)
|
||||
.values(self.resource_field_name)
|
||||
.annotate(roles_array=ArrayAgg("role"))
|
||||
.values("roles_array")
|
||||
)
|
||||
|
||||
# Limit to resource access instances related to a resource THAT also has
|
||||
# a resource access
|
||||
# instance for the logged-in user (we don't want to list only the resource
|
||||
# access instances pointing to the logged-in user)
|
||||
queryset = (
|
||||
queryset.filter(
|
||||
db.Q(**{f"{self.resource_field_name}__accesses__user": user})
|
||||
| db.Q(
|
||||
**{f"{self.resource_field_name}__accesses__team__in": teams}
|
||||
),
|
||||
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||
)
|
||||
.annotate(user_roles=db.Subquery(user_roles_query))
|
||||
.distinct()
|
||||
)
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Forbid deleting the last owner access"""
|
||||
instance = self.get_object()
|
||||
resource = getattr(instance, self.resource_field_name)
|
||||
|
||||
# Check if the access being deleted is the last owner access for the resource
|
||||
if (
|
||||
instance.role == "owner"
|
||||
and resource.accesses.filter(role="owner").count() == 1
|
||||
):
|
||||
return drf.response.Response(
|
||||
{"detail": "Cannot delete the last owner access for the resource."},
|
||||
status=drf.status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Check that we don't change the role if it leads to losing the last owner."""
|
||||
instance = serializer.instance
|
||||
|
||||
# Check if the role is being updated and the new role is not "owner"
|
||||
if (
|
||||
"role" in self.request.data
|
||||
and self.request.data["role"] != models.RoleChoices.OWNER
|
||||
):
|
||||
resource = getattr(instance, self.resource_field_name)
|
||||
# Check if the access being updated is the last owner access for the resource
|
||||
if (
|
||||
instance.role == models.RoleChoices.OWNER
|
||||
and resource.accesses.filter(role=models.RoleChoices.OWNER).count() == 1
|
||||
):
|
||||
message = "Cannot change the role to a non-owner role for the last owner access."
|
||||
raise drf.exceptions.PermissionDenied({"detail": message})
|
||||
|
||||
serializer.save()
|
||||
|
||||
|
||||
class DocumentMetadata(drf.metadata.SimpleMetadata):
|
||||
"""Custom metadata class to add information"""
|
||||
@@ -434,7 +357,7 @@ class DocumentViewSet(
|
||||
ordering_fields = ["created_at", "updated_at", "title"]
|
||||
pagination_class = Pagination
|
||||
permission_classes = [
|
||||
permissions.DocumentAccessPermission,
|
||||
permissions.DocumentPermission,
|
||||
]
|
||||
queryset = models.Document.objects.all()
|
||||
serializer_class = serializers.DocumentSerializer
|
||||
@@ -445,44 +368,6 @@ class DocumentViewSet(
|
||||
trashbin_serializer_class = serializers.ListDocumentSerializer
|
||||
tree_serializer_class = serializers.ListDocumentSerializer
|
||||
|
||||
def annotate_is_favorite(self, queryset):
|
||||
"""
|
||||
Annotate document queryset with the favorite status for the current user.
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
if user.is_authenticated:
|
||||
favorite_exists_subquery = models.DocumentFavorite.objects.filter(
|
||||
document_id=db.OuterRef("pk"), user=user
|
||||
)
|
||||
return queryset.annotate(is_favorite=db.Exists(favorite_exists_subquery))
|
||||
|
||||
return queryset.annotate(is_favorite=db.Value(False))
|
||||
|
||||
def annotate_user_roles(self, queryset):
|
||||
"""
|
||||
Annotate document queryset with the roles of the current user
|
||||
on the document or its ancestors.
|
||||
"""
|
||||
user = self.request.user
|
||||
output_field = ArrayField(base_field=db.CharField())
|
||||
|
||||
if user.is_authenticated:
|
||||
user_roles_subquery = models.DocumentAccess.objects.filter(
|
||||
db.Q(user=user) | db.Q(team__in=user.teams),
|
||||
document__path=Left(db.OuterRef("path"), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
return queryset.annotate(
|
||||
user_roles=db.Func(
|
||||
user_roles_subquery, function="ARRAY", output_field=output_field
|
||||
)
|
||||
)
|
||||
|
||||
return queryset.annotate(
|
||||
user_roles=db.Value([], output_field=output_field),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get queryset performing all annotation and filtering on the document tree structure."""
|
||||
user = self.request.user
|
||||
@@ -518,18 +403,20 @@ class DocumentViewSet(
|
||||
def filter_queryset(self, queryset):
|
||||
"""Override to apply annotations to generic views."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
user = self.request.user
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
return queryset
|
||||
|
||||
def get_response_for_queryset(self, queryset):
|
||||
def get_response_for_queryset(self, queryset, context=None):
|
||||
"""Return paginated response for the queryset if requested."""
|
||||
context = context or self.get_serializer_context()
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
serializer = self.get_serializer(page, many=True, context=context)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
serializer = self.get_serializer(queryset, many=True, context=context)
|
||||
return drf.response.Response(serializer.data)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
@@ -539,13 +426,11 @@ class DocumentViewSet(
|
||||
This method applies filtering based on request parameters using `ListDocumentFilter`.
|
||||
It performs early filtering on model fields, annotates user roles, and removes
|
||||
descendant documents to keep only the highest ancestors readable by the current user.
|
||||
|
||||
Additional annotations (e.g., `is_highest_ancestor_for_user`, favorite status) are
|
||||
applied before ordering and returning the response.
|
||||
"""
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
) # Not calling filter_queryset. We do our own cooking.
|
||||
user = self.request.user
|
||||
|
||||
# Not calling filter_queryset. We do our own cooking.
|
||||
queryset = self.get_queryset()
|
||||
|
||||
filterset = ListDocumentFilter(
|
||||
self.request.GET, queryset=queryset, request=self.request
|
||||
@@ -558,7 +443,7 @@ class DocumentViewSet(
|
||||
for field in ["is_creator_me", "title"]:
|
||||
queryset = filterset.filters[field].filter(queryset, filter_data[field])
|
||||
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
|
||||
# Among the results, we may have documents that are ancestors/descendants
|
||||
# of each other. In this case we want to keep only the highest ancestors.
|
||||
@@ -568,14 +453,8 @@ class DocumentViewSet(
|
||||
)
|
||||
queryset = queryset.filter(path__in=root_paths)
|
||||
|
||||
# Annotate the queryset with an attribute marking instances as highest ancestor
|
||||
# in order to save some time while computing abilities on the instance
|
||||
queryset = queryset.annotate(
|
||||
is_highest_ancestor_for_user=db.Value(True, output_field=db.BooleanField())
|
||||
)
|
||||
|
||||
# Annotate favorite status and filter if applicable as late as possible
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
queryset = filterset.filters["is_favorite"].filter(
|
||||
queryset, filter_data["is_favorite"]
|
||||
)
|
||||
@@ -666,7 +545,7 @@ class DocumentViewSet(
|
||||
deleted_at__isnull=False,
|
||||
deleted_at__gte=models.get_trashbin_cutoff(),
|
||||
)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
queryset = queryset.annotate_user_roles(self.request.user)
|
||||
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
|
||||
|
||||
return self.get_response_for_queryset(queryset)
|
||||
@@ -734,7 +613,7 @@ class DocumentViewSet(
|
||||
|
||||
position = validated_data["position"]
|
||||
message = None
|
||||
|
||||
owner_accesses = []
|
||||
if position in [
|
||||
enums.MoveNodePositionChoices.FIRST_CHILD,
|
||||
enums.MoveNodePositionChoices.LAST_CHILD,
|
||||
@@ -744,12 +623,15 @@ class DocumentViewSet(
|
||||
"You do not have permission to move documents "
|
||||
"as a child to this target document."
|
||||
)
|
||||
elif not target_document.is_root():
|
||||
if not target_document.get_parent().get_abilities(user).get("move"):
|
||||
message = (
|
||||
"You do not have permission to move documents "
|
||||
"as a sibling of this target document."
|
||||
)
|
||||
elif target_document.is_root():
|
||||
owner_accesses = document.get_root().accesses.filter(
|
||||
role=models.RoleChoices.OWNER
|
||||
)
|
||||
elif not target_document.get_parent().get_abilities(user).get("move"):
|
||||
message = (
|
||||
"You do not have permission to move documents "
|
||||
"as a sibling of this target document."
|
||||
)
|
||||
|
||||
if message:
|
||||
return drf.response.Response(
|
||||
@@ -759,6 +641,19 @@ class DocumentViewSet(
|
||||
|
||||
document.move(target_document, pos=position)
|
||||
|
||||
# Make sure we have at least one owner
|
||||
if (
|
||||
owner_accesses
|
||||
and not document.accesses.filter(role=models.RoleChoices.OWNER).exists()
|
||||
):
|
||||
for owner_access in owner_accesses:
|
||||
models.DocumentAccess.objects.update_or_create(
|
||||
document=document,
|
||||
user=owner_access.user,
|
||||
team=owner_access.team,
|
||||
defaults={"role": models.RoleChoices.OWNER},
|
||||
)
|
||||
|
||||
return drf.response.Response(
|
||||
{"message": "Document moved successfully."}, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -805,11 +700,7 @@ class DocumentViewSet(
|
||||
creator=request.user,
|
||||
**serializer.validated_data,
|
||||
)
|
||||
models.DocumentAccess.objects.create(
|
||||
document=child_document,
|
||||
user=request.user,
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
|
||||
# Set the created instance to the serializer
|
||||
serializer.instance = child_document
|
||||
|
||||
@@ -828,7 +719,17 @@ class DocumentViewSet(
|
||||
|
||||
queryset = filterset.qs
|
||||
|
||||
return self.get_response_for_queryset(queryset)
|
||||
# Pass ancestors' links paths mapping to the serializer as a context variable
|
||||
# in order to allow saving time while computing abilities on the instance
|
||||
paths_links_mapping = document.compute_ancestors_links_paths_mapping()
|
||||
|
||||
return self.get_response_for_queryset(
|
||||
queryset,
|
||||
context={
|
||||
"request": request,
|
||||
"paths_links_mapping": paths_links_mapping,
|
||||
},
|
||||
)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
@@ -860,10 +761,12 @@ class DocumentViewSet(
|
||||
List ancestors tree above the document.
|
||||
What we need to display is the tree structure opened for the current document.
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
try:
|
||||
current_document = self.queryset.only("depth", "path").get(pk=pk)
|
||||
except models.Document.DoesNotExist as excpt:
|
||||
raise drf.exceptions.NotFound from excpt
|
||||
raise drf.exceptions.NotFound() from excpt
|
||||
|
||||
ancestors = (
|
||||
(current_document.get_ancestors() | self.queryset.filter(pk=pk))
|
||||
@@ -885,13 +788,6 @@ class DocumentViewSet(
|
||||
ancestors_links = []
|
||||
children_clause = db.Q()
|
||||
for ancestor in ancestors:
|
||||
if ancestor.depth < highest_readable.depth:
|
||||
continue
|
||||
|
||||
children_clause |= db.Q(
|
||||
path__startswith=ancestor.path, depth=ancestor.depth + 1
|
||||
)
|
||||
|
||||
# Compute cache for ancestors links to avoid many queries while computing
|
||||
# abilities for his documents in the tree!
|
||||
ancestors_links.append(
|
||||
@@ -899,25 +795,21 @@ class DocumentViewSet(
|
||||
)
|
||||
paths_links_mapping[ancestor.path] = ancestors_links.copy()
|
||||
|
||||
if ancestor.depth < highest_readable.depth:
|
||||
continue
|
||||
|
||||
children_clause |= db.Q(
|
||||
path__startswith=ancestor.path, depth=ancestor.depth + 1
|
||||
)
|
||||
|
||||
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
|
||||
|
||||
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
|
||||
queryset = queryset.order_by("path")
|
||||
# Annotate if the current document is the highest ancestor for the user
|
||||
queryset = queryset.annotate(
|
||||
is_highest_ancestor_for_user=db.Case(
|
||||
db.When(
|
||||
path=db.Value(highest_readable.path),
|
||||
then=db.Value(True),
|
||||
),
|
||||
default=db.Value(False),
|
||||
output_field=db.BooleanField(),
|
||||
)
|
||||
)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
|
||||
# Pass ancestors' links definitions to the serializer as a context variable
|
||||
# Pass ancestors' links paths mapping to the serializer as a context variable
|
||||
# in order to allow saving time while computing abilities on the instance
|
||||
serializer = self.get_serializer(
|
||||
queryset,
|
||||
@@ -934,7 +826,10 @@ class DocumentViewSet(
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
permission_classes=[permissions.IsAuthenticated, permissions.AccessPermission],
|
||||
permission_classes=[
|
||||
permissions.IsAuthenticated,
|
||||
permissions.DocumentPermission,
|
||||
],
|
||||
url_path="duplicate",
|
||||
)
|
||||
@transaction.atomic
|
||||
@@ -1472,7 +1367,11 @@ class DocumentViewSet(
|
||||
|
||||
class DocumentAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
viewsets.ModelViewSet,
|
||||
drf.mixins.CreateModelMixin,
|
||||
drf.mixins.RetrieveModelMixin,
|
||||
drf.mixins.UpdateModelMixin,
|
||||
drf.mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""
|
||||
API ViewSet for all interactions with document accesses.
|
||||
@@ -1499,50 +1398,146 @@ class DocumentAccessViewSet(
|
||||
"""
|
||||
|
||||
lookup_field = "pk"
|
||||
pagination_class = Pagination
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
||||
queryset = models.DocumentAccess.objects.select_related("user").all()
|
||||
permission_classes = [permissions.ResourceAccessPermission]
|
||||
queryset = models.DocumentAccess.objects.select_related("user", "document").only(
|
||||
"id",
|
||||
"created_at",
|
||||
"role",
|
||||
"team",
|
||||
"user__id",
|
||||
"user__short_name",
|
||||
"user__full_name",
|
||||
"user__email",
|
||||
"user__language",
|
||||
"document__id",
|
||||
"document__path",
|
||||
"document__depth",
|
||||
)
|
||||
resource_field_name = "document"
|
||||
serializer_class = serializers.DocumentAccessSerializer
|
||||
is_current_user_owner_or_admin = False
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
if self.action == "list":
|
||||
try:
|
||||
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
|
||||
except models.Document.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
roles = set(document.get_roles(self.request.user))
|
||||
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
|
||||
self.is_current_user_owner_or_admin = is_owner_or_admin
|
||||
if not is_owner_or_admin:
|
||||
# Return only the document owner access
|
||||
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
|
||||
|
||||
return queryset
|
||||
@cached_property
|
||||
def document(self):
|
||||
"""Get related document from resource ID in url and annotate user roles."""
|
||||
try:
|
||||
return models.Document.objects.annotate_user_roles(self.request.user).get(
|
||||
pk=self.kwargs["resource_id"]
|
||||
)
|
||||
except models.Document.DoesNotExist as excpt:
|
||||
raise drf.exceptions.NotFound() from excpt
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list" and not self.is_current_user_owner_or_admin:
|
||||
return serializers.DocumentAccessLightSerializer
|
||||
"""Use light serializer for unprivileged users."""
|
||||
return (
|
||||
serializers.DocumentAccessSerializer
|
||||
if self.document.get_role(self.request.user) in choices.PRIVILEGED_ROLES
|
||||
else serializers.DocumentAccessLightSerializer
|
||||
)
|
||||
|
||||
return super().get_serializer_class()
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Return accesses for the current document with filters and annotations."""
|
||||
user = request.user
|
||||
|
||||
role = self.document.get_role(user)
|
||||
if not role:
|
||||
return drf.response.Response([])
|
||||
|
||||
ancestors = (
|
||||
self.document.get_ancestors()
|
||||
| models.Document.objects.filter(pk=self.document.pk)
|
||||
).filter(ancestors_deleted_at__isnull=True)
|
||||
|
||||
queryset = self.get_queryset().filter(document__in=ancestors)
|
||||
|
||||
if role not in choices.PRIVILEGED_ROLES:
|
||||
queryset = queryset.filter(role__in=choices.PRIVILEGED_ROLES)
|
||||
|
||||
accesses = list(queryset.order_by("document__path"))
|
||||
|
||||
# Annotate more information on roles
|
||||
# - accesses of the user (direct or via a team)
|
||||
path_to_ancestors_roles = defaultdict(list)
|
||||
path_to_role = defaultdict(lambda: None)
|
||||
# - accesses of other users and teams
|
||||
key_to_path_to_max_ancestors_role = defaultdict(
|
||||
lambda: defaultdict(lambda: None)
|
||||
)
|
||||
for access in accesses:
|
||||
key = access.target_key
|
||||
path = access.document.path
|
||||
parent_path = path[: -models.Document.steplen]
|
||||
|
||||
if parent_path:
|
||||
key_to_path_to_max_ancestors_role[key][parent_path] = (
|
||||
choices.RoleChoices.max(
|
||||
*key_to_path_to_max_ancestors_role[key].values()
|
||||
)
|
||||
)
|
||||
path_to_ancestors_roles[path].extend(
|
||||
path_to_ancestors_roles[parent_path]
|
||||
)
|
||||
path_to_ancestors_roles[path].append(path_to_role[parent_path])
|
||||
else:
|
||||
path_to_ancestors_roles[path] = []
|
||||
|
||||
key_to_path_to_max_ancestors_role[key][path] = choices.RoleChoices.max(
|
||||
key_to_path_to_max_ancestors_role[key][parent_path], access.role
|
||||
)
|
||||
|
||||
if access.user_id == user.id or access.team in user.teams:
|
||||
path_to_role[path] = choices.RoleChoices.max(
|
||||
path_to_role[path], access.role
|
||||
)
|
||||
|
||||
# serialize and return the response
|
||||
context = self.get_serializer_context()
|
||||
serializer_class = self.get_serializer_class()
|
||||
serialized_data = []
|
||||
for access in accesses:
|
||||
path = access.document.path
|
||||
parent_path = path[: -models.Document.steplen]
|
||||
access.max_ancestors_role = (
|
||||
key_to_path_to_max_ancestors_role[access.target_key][parent_path]
|
||||
if parent_path
|
||||
else None
|
||||
)
|
||||
access.set_user_roles_tuple(
|
||||
choices.RoleChoices.max(*path_to_ancestors_roles[path]),
|
||||
path_to_role.get(path),
|
||||
)
|
||||
serializer = serializer_class(access, context=context)
|
||||
serialized_data.append(serializer.data)
|
||||
|
||||
return drf.response.Response(serialized_data)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Add a new access to the document and send an email to the new added user."""
|
||||
access = serializer.save()
|
||||
"""
|
||||
Actually create the new document access:
|
||||
- Ensures the `document_id` is explicitly set from the URL
|
||||
- If the assigned role is `OWNER`, checks that the requesting user is an owner
|
||||
of the document. This is the only permission check deferred until this step;
|
||||
all other access checks are handled earlier in the permission lifecycle.
|
||||
- Sends an invitation email to the newly added user after saving the access.
|
||||
"""
|
||||
role = serializer.validated_data.get("role")
|
||||
if (
|
||||
role == choices.RoleChoices.OWNER
|
||||
and self.document.get_role(self.request.user) != choices.RoleChoices.OWNER
|
||||
):
|
||||
raise drf.exceptions.PermissionDenied(
|
||||
"Only owners of a document can assign other users as owners."
|
||||
)
|
||||
|
||||
access.document.send_invitation_email(
|
||||
access.user.email,
|
||||
access.role,
|
||||
self.request.user,
|
||||
access.user.language
|
||||
or self.request.user.language
|
||||
or settings.LANGUAGE_CODE,
|
||||
)
|
||||
access = serializer.save(document_id=self.kwargs["resource_id"])
|
||||
|
||||
if access.user:
|
||||
access.document.send_invitation_email(
|
||||
access.user.email,
|
||||
access.role,
|
||||
self.request.user,
|
||||
access.user.language
|
||||
or self.request.user.language
|
||||
or settings.LANGUAGE_CODE,
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Update an access to the document and notify the collaboration server."""
|
||||
@@ -1579,7 +1574,7 @@ class TemplateViewSet(
|
||||
filter_backends = [drf.filters.OrderingFilter]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticatedOrSafe,
|
||||
permissions.AccessPermission,
|
||||
permissions.ResourceWithAccessPermission,
|
||||
]
|
||||
ordering = ["-created_at"]
|
||||
ordering_fields = ["created_at", "updated_at", "title"]
|
||||
@@ -1641,7 +1636,6 @@ class TemplateAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
drf.mixins.CreateModelMixin,
|
||||
drf.mixins.DestroyModelMixin,
|
||||
drf.mixins.ListModelMixin,
|
||||
drf.mixins.RetrieveModelMixin,
|
||||
drf.mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
@@ -1671,12 +1665,55 @@ class TemplateAccessViewSet(
|
||||
"""
|
||||
|
||||
lookup_field = "pk"
|
||||
pagination_class = Pagination
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
||||
permission_classes = [permissions.ResourceAccessPermission]
|
||||
queryset = models.TemplateAccess.objects.select_related("user").all()
|
||||
resource_field_name = "template"
|
||||
serializer_class = serializers.TemplateAccessSerializer
|
||||
|
||||
@cached_property
|
||||
def template(self):
|
||||
"""Get related template from resource ID in url."""
|
||||
try:
|
||||
return models.Template.objects.get(pk=self.kwargs["resource_id"])
|
||||
except models.Template.DoesNotExist as excpt:
|
||||
raise drf.exceptions.NotFound() from excpt
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Restrict templates returned by the list endpoint"""
|
||||
user = self.request.user
|
||||
teams = user.teams
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Limit to resource access instances related to a resource THAT also has
|
||||
# a resource access instance for the logged-in user (we don't want to list
|
||||
# only the resource access instances pointing to the logged-in user)
|
||||
queryset = queryset.filter(
|
||||
db.Q(template__accesses__user=user)
|
||||
| db.Q(template__accesses__team__in=teams),
|
||||
).distinct()
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return drf.response.Response(serializer.data)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Actually create the new template access:
|
||||
- Ensures the `template_id` is explicitly set from the URL.
|
||||
- If the assigned role is `OWNER`, checks that the requesting user is an owner
|
||||
of the document. This is the only permission check deferred until this step;
|
||||
all other access checks are handled earlier in the permission lifecycle.
|
||||
"""
|
||||
role = serializer.validated_data.get("role")
|
||||
if (
|
||||
role == choices.RoleChoices.OWNER
|
||||
and self.template.get_role(self.request.user) != choices.RoleChoices.OWNER
|
||||
):
|
||||
raise drf.exceptions.PermissionDenied(
|
||||
"Only owners of a template can assign other users as owners."
|
||||
)
|
||||
|
||||
serializer.save(template_id=self.kwargs["resource_id"])
|
||||
|
||||
|
||||
class InvitationViewset(
|
||||
drf.mixins.CreateModelMixin,
|
||||
@@ -1709,7 +1746,7 @@ class InvitationViewset(
|
||||
pagination_class = Pagination
|
||||
permission_classes = [
|
||||
permissions.CanCreateInvitationPermission,
|
||||
permissions.AccessPermission,
|
||||
permissions.ResourceWithAccessPermission,
|
||||
]
|
||||
queryset = (
|
||||
models.Invitation.objects.all()
|
||||
@@ -1718,6 +1755,16 @@ class InvitationViewset(
|
||||
)
|
||||
serializer_class = serializers.InvitationSerializer
|
||||
|
||||
@cached_property
|
||||
def document(self):
|
||||
"""Get related document from resource ID in url and annotate user roles."""
|
||||
try:
|
||||
return models.Document.objects.annotate_user_roles(self.request.user).get(
|
||||
pk=self.kwargs["resource_id"]
|
||||
)
|
||||
except models.Document.DoesNotExist as excpt:
|
||||
raise drf.exceptions.NotFound() from excpt
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extra context provided to the serializer class."""
|
||||
context = super().get_serializer_context()
|
||||
@@ -1749,11 +1796,11 @@ class InvitationViewset(
|
||||
queryset.filter(
|
||||
db.Q(
|
||||
document__accesses__user=user,
|
||||
document__accesses__role__in=models.PRIVILEGED_ROLES,
|
||||
document__accesses__role__in=choices.PRIVILEGED_ROLES,
|
||||
)
|
||||
| db.Q(
|
||||
document__accesses__team__in=teams,
|
||||
document__accesses__role__in=models.PRIVILEGED_ROLES,
|
||||
document__accesses__role__in=choices.PRIVILEGED_ROLES,
|
||||
),
|
||||
)
|
||||
# Abilities are computed based on logged-in user's role and
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Declare and configure choices for Docs' core application."""
|
||||
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class PriorityTextChoices(TextChoices):
|
||||
"""
|
||||
This class inherits from Django's TextChoices and provides a method to get the priority
|
||||
of a given value based on its position in the class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_priority(cls, role):
|
||||
"""Returns the priority of the given role based on its order in the class."""
|
||||
|
||||
members = list(cls.__members__.values())
|
||||
return members.index(role) + 1 if role in members else 0
|
||||
|
||||
@classmethod
|
||||
def max(cls, *roles):
|
||||
"""
|
||||
Return the highest-priority role among the given roles, using get_priority().
|
||||
If no valid roles are provided, returns None.
|
||||
"""
|
||||
valid_roles = [role for role in roles if cls.get_priority(role) is not None]
|
||||
if not valid_roles:
|
||||
return None
|
||||
return max(valid_roles, key=cls.get_priority)
|
||||
|
||||
|
||||
class LinkRoleChoices(PriorityTextChoices):
|
||||
"""Defines the possible roles a link can offer on a document."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
|
||||
|
||||
class RoleChoices(PriorityTextChoices):
|
||||
"""Defines the possible roles a user can have in a resource."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
||||
OWNER = "owner", _("Owner")
|
||||
|
||||
|
||||
PRIVILEGED_ROLES = [RoleChoices.ADMIN, RoleChoices.OWNER]
|
||||
|
||||
|
||||
class LinkReachChoices(PriorityTextChoices):
|
||||
"""Defines types of access for links"""
|
||||
|
||||
RESTRICTED = (
|
||||
"restricted",
|
||||
_("Restricted"),
|
||||
) # Only users with a specific access can read/edit the document
|
||||
AUTHENTICATED = (
|
||||
"authenticated",
|
||||
_("Authenticated"),
|
||||
) # Any authenticated user can access the document
|
||||
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||
|
||||
@classmethod
|
||||
def get_select_options(cls, link_reach, link_role):
|
||||
"""
|
||||
Determines the valid select options for link reach and link role depending on the
|
||||
ancestors' link reach/role given as arguments.
|
||||
Returns:
|
||||
Dictionary mapping possible reach levels to their corresponding possible roles.
|
||||
"""
|
||||
return {
|
||||
reach: [
|
||||
role
|
||||
for role in LinkRoleChoices.values
|
||||
if LinkRoleChoices.get_priority(role)
|
||||
>= LinkRoleChoices.get_priority(link_role)
|
||||
]
|
||||
if reach != cls.RESTRICTED
|
||||
else None
|
||||
for reach in cls.values
|
||||
if LinkReachChoices.get_priority(reach)
|
||||
>= LinkReachChoices.get_priority(link_reach)
|
||||
}
|
||||
|
||||
|
||||
def get_equivalent_link_definition(ancestors_links):
|
||||
"""
|
||||
Return the (reach, role) pair with:
|
||||
1. Highest reach
|
||||
2. Highest role among links having that reach
|
||||
"""
|
||||
if not ancestors_links:
|
||||
return {"link_reach": None, "link_role": None}
|
||||
|
||||
# 1) Find the highest reach
|
||||
max_reach = max(
|
||||
ancestors_links,
|
||||
key=lambda link: LinkReachChoices.get_priority(link["link_reach"]),
|
||||
)["link_reach"]
|
||||
|
||||
# 2) Among those, find the highest role (ignore role if RESTRICTED)
|
||||
if max_reach == LinkReachChoices.RESTRICTED:
|
||||
max_role = None
|
||||
else:
|
||||
max_role = max(
|
||||
(
|
||||
link["link_role"]
|
||||
for link in ancestors_links
|
||||
if link["link_reach"] == max_reach
|
||||
),
|
||||
key=LinkRoleChoices.get_priority,
|
||||
)
|
||||
|
||||
return {"link_reach": max_reach, "link_role": max_role}
|
||||
+303
-232
@@ -6,7 +6,6 @@ Declare and configure the models for the impress core application
|
||||
import hashlib
|
||||
import smtplib
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from logging import getLogger
|
||||
|
||||
@@ -33,6 +32,14 @@ from rest_framework.exceptions import ValidationError
|
||||
from timezone_field import TimeZoneField
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
|
||||
|
||||
from .choices import (
|
||||
PRIVILEGED_ROLES,
|
||||
LinkReachChoices,
|
||||
LinkRoleChoices,
|
||||
RoleChoices,
|
||||
get_equivalent_link_definition,
|
||||
)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@@ -50,88 +57,6 @@ def get_trashbin_cutoff():
|
||||
return timezone.now() - timedelta(days=settings.TRASHBIN_CUTOFF_DAYS)
|
||||
|
||||
|
||||
class LinkRoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a link can offer on a document."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
|
||||
|
||||
class RoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a user can have in a resource."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
||||
OWNER = "owner", _("Owner")
|
||||
|
||||
|
||||
PRIVILEGED_ROLES = [RoleChoices.ADMIN, RoleChoices.OWNER]
|
||||
|
||||
|
||||
class LinkReachChoices(models.TextChoices):
|
||||
"""Defines types of access for links"""
|
||||
|
||||
RESTRICTED = (
|
||||
"restricted",
|
||||
_("Restricted"),
|
||||
) # Only users with a specific access can read/edit the document
|
||||
AUTHENTICATED = (
|
||||
"authenticated",
|
||||
_("Authenticated"),
|
||||
) # Any authenticated user can access the document
|
||||
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||
|
||||
@classmethod
|
||||
def get_select_options(cls, ancestors_links):
|
||||
"""
|
||||
Determines the valid select options for link reach and link role depending on the
|
||||
list of ancestors' link reach/role.
|
||||
|
||||
Args:
|
||||
ancestors_links: List of dictionaries, each with 'link_reach' and 'link_role' keys
|
||||
representing the reach and role of ancestors links.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping possible reach levels to their corresponding possible roles.
|
||||
"""
|
||||
# If no ancestors, return all options
|
||||
if not ancestors_links:
|
||||
return dict.fromkeys(cls.values, LinkRoleChoices.values)
|
||||
|
||||
# Initialize result with all possible reaches and role options as sets
|
||||
result = {reach: set(LinkRoleChoices.values) for reach in cls.values}
|
||||
|
||||
# Group roles by reach level
|
||||
reach_roles = defaultdict(set)
|
||||
for link in ancestors_links:
|
||||
reach_roles[link["link_reach"]].add(link["link_role"])
|
||||
|
||||
# Apply constraints based on ancestor links
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.RESTRICTED]:
|
||||
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
|
||||
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.AUTHENTICATED]:
|
||||
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
elif LinkRoleChoices.READER in reach_roles[cls.AUTHENTICATED]:
|
||||
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
|
||||
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.PUBLIC]:
|
||||
result[cls.PUBLIC].discard(LinkRoleChoices.READER)
|
||||
result.pop(cls.AUTHENTICATED, None)
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
elif LinkRoleChoices.READER in reach_roles[cls.PUBLIC]:
|
||||
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
|
||||
result.get(cls.RESTRICTED, set()).discard(LinkRoleChoices.READER)
|
||||
|
||||
# Convert roles sets to lists while maintaining the order from LinkRoleChoices
|
||||
for reach, roles in result.items():
|
||||
result[reach] = [role for role in LinkRoleChoices.values if role in roles]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class DuplicateEmailError(Exception):
|
||||
"""Raised when an email is already associated with a pre-existing user."""
|
||||
|
||||
@@ -364,69 +289,6 @@ class BaseAccess(BaseModel):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def _get_roles(self, resource, user):
|
||||
"""
|
||||
Get the roles a user has on a resource.
|
||||
"""
|
||||
roles = []
|
||||
if user.is_authenticated:
|
||||
teams = user.teams
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
except AttributeError:
|
||||
try:
|
||||
roles = resource.accesses.filter(
|
||||
models.Q(user=user) | models.Q(team__in=teams),
|
||||
).values_list("role", flat=True)
|
||||
except (self._meta.model.DoesNotExist, IndexError):
|
||||
roles = []
|
||||
|
||||
return roles
|
||||
|
||||
def _get_abilities(self, resource, user):
|
||||
"""
|
||||
Compute and return abilities for a given user taking into account
|
||||
the current state of the object.
|
||||
"""
|
||||
roles = self._get_roles(resource, user)
|
||||
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (
|
||||
RoleChoices.OWNER in roles
|
||||
and resource.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||
)
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
)
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if RoleChoices.OWNER in roles:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to),
|
||||
"partial_update": bool(set_role_to),
|
||||
"retrieve": bool(roles),
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
|
||||
class DocumentQuerySet(MP_NodeQuerySet):
|
||||
"""
|
||||
@@ -452,6 +314,41 @@ class DocumentQuerySet(MP_NodeQuerySet):
|
||||
|
||||
return self.filter(link_reach=LinkReachChoices.PUBLIC)
|
||||
|
||||
def annotate_is_favorite(self, user):
|
||||
"""
|
||||
Annotate document queryset with the favorite status for the current user.
|
||||
"""
|
||||
if user.is_authenticated:
|
||||
favorite_exists_subquery = DocumentFavorite.objects.filter(
|
||||
document_id=models.OuterRef("pk"), user=user
|
||||
)
|
||||
return self.annotate(is_favorite=models.Exists(favorite_exists_subquery))
|
||||
|
||||
return self.annotate(is_favorite=models.Value(False))
|
||||
|
||||
def annotate_user_roles(self, user):
|
||||
"""
|
||||
Annotate document queryset with the roles of the current user
|
||||
on the document or its ancestors.
|
||||
"""
|
||||
output_field = ArrayField(base_field=models.CharField())
|
||||
|
||||
if user.is_authenticated:
|
||||
user_roles_subquery = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(models.OuterRef("path"), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
return self.annotate(
|
||||
user_roles=models.Func(
|
||||
user_roles_subquery, function="ARRAY", output_field=output_field
|
||||
)
|
||||
)
|
||||
|
||||
return self.annotate(
|
||||
user_roles=models.Value([], output_field=output_field),
|
||||
)
|
||||
|
||||
|
||||
class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
"""
|
||||
@@ -464,6 +361,7 @@ class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
return self._queryset_class(self.model).order_by("path")
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class Document(MP_Node, BaseModel):
|
||||
"""Pad document carrying the content."""
|
||||
|
||||
@@ -531,6 +429,12 @@ class Document(MP_Node, BaseModel):
|
||||
def __str__(self):
|
||||
return str(self.title) if self.title else str(_("Untitled Document"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize cache property."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._ancestors_link_definition = None
|
||||
self._computed_link_definition = None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Write content to object storage only if _content has changed."""
|
||||
super().save(*args, **kwargs)
|
||||
@@ -718,38 +622,22 @@ class Document(MP_Node, BaseModel):
|
||||
cache_key = document.get_nb_accesses_cache_key()
|
||||
cache.delete(cache_key)
|
||||
|
||||
def get_roles(self, user):
|
||||
def get_role(self, user):
|
||||
"""Return the roles a user has on a document."""
|
||||
if not user.is_authenticated:
|
||||
return []
|
||||
return None
|
||||
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
except AttributeError:
|
||||
try:
|
||||
roles = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(
|
||||
models.Value(self.path), Length("document__path")
|
||||
),
|
||||
).values_list("role", flat=True)
|
||||
except (models.ObjectDoesNotExist, IndexError):
|
||||
roles = []
|
||||
return roles
|
||||
roles = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(models.Value(self.path), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
def get_links_definitions(self, ancestors_links):
|
||||
"""Get links reach/role definitions for the current document and its ancestors."""
|
||||
return RoleChoices.max(*roles)
|
||||
|
||||
links_definitions = defaultdict(set)
|
||||
links_definitions[self.link_reach].add(self.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 default dict back to a normal dict
|
||||
|
||||
def compute_ancestors_links(self, user):
|
||||
def compute_ancestors_links_paths_mapping(self):
|
||||
"""
|
||||
Compute the ancestors links for the current document up to the highest readable ancestor.
|
||||
"""
|
||||
@@ -758,63 +646,114 @@ class Document(MP_Node, BaseModel):
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
highest_readable = ancestors.readable_per_se(user).only("depth").first()
|
||||
|
||||
if highest_readable is None:
|
||||
return []
|
||||
|
||||
ancestors_links = []
|
||||
paths_links_mapping = {}
|
||||
for ancestor in ancestors.filter(depth__gte=highest_readable.depth):
|
||||
|
||||
for ancestor in ancestors:
|
||||
ancestors_links.append(
|
||||
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
|
||||
)
|
||||
paths_links_mapping[ancestor.path] = ancestors_links.copy()
|
||||
|
||||
ancestors_links = paths_links_mapping.get(self.path[: -self.steplen], [])
|
||||
return paths_links_mapping
|
||||
|
||||
return ancestors_links
|
||||
@property
|
||||
def link_definition(self):
|
||||
"""Returns link reach/role as a definition in dictionary format."""
|
||||
return {"link_reach": self.link_reach, "link_role": self.link_role}
|
||||
|
||||
def get_abilities(self, user, ancestors_links=None):
|
||||
@property
|
||||
def ancestors_link_definition(self):
|
||||
"""Link definition equivalent to all document's ancestors."""
|
||||
if getattr(self, "_ancestors_link_definition", None) is None:
|
||||
if self.depth <= 1:
|
||||
ancestors_links = []
|
||||
else:
|
||||
mapping = self.compute_ancestors_links_paths_mapping()
|
||||
ancestors_links = mapping.get(self.path[: -self.steplen], [])
|
||||
self._ancestors_link_definition = get_equivalent_link_definition(
|
||||
ancestors_links
|
||||
)
|
||||
|
||||
return self._ancestors_link_definition
|
||||
|
||||
@ancestors_link_definition.setter
|
||||
def ancestors_link_definition(self, definition):
|
||||
"""Cache the ancestors_link_definition."""
|
||||
self._ancestors_link_definition = definition
|
||||
|
||||
@property
|
||||
def ancestors_link_reach(self):
|
||||
"""Link reach equivalent to all document's ancestors."""
|
||||
return self.ancestors_link_definition["link_reach"]
|
||||
|
||||
@property
|
||||
def ancestors_link_role(self):
|
||||
"""Link role equivalent to all document's ancestors."""
|
||||
return self.ancestors_link_definition["link_role"]
|
||||
|
||||
@property
|
||||
def computed_link_definition(self):
|
||||
"""
|
||||
Link reach/role on the document, combining inherited ancestors' link
|
||||
definitions and the document's own link definition.
|
||||
"""
|
||||
if getattr(self, "_computed_link_definition", None) is None:
|
||||
self._computed_link_definition = get_equivalent_link_definition(
|
||||
[self.ancestors_link_definition, self.link_definition]
|
||||
)
|
||||
return self._computed_link_definition
|
||||
|
||||
@property
|
||||
def computed_link_reach(self):
|
||||
"""Actual link reach on the document."""
|
||||
return self.computed_link_definition["link_reach"]
|
||||
|
||||
@property
|
||||
def computed_link_role(self):
|
||||
"""Actual link role on the document."""
|
||||
return self.computed_link_definition["link_role"]
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the document.
|
||||
"""
|
||||
if self.depth <= 1 or getattr(self, "is_highest_ancestor_for_user", False):
|
||||
ancestors_links = []
|
||||
elif ancestors_links is None:
|
||||
ancestors_links = self.compute_ancestors_links(user=user)
|
||||
|
||||
roles = set(
|
||||
self.get_roles(user)
|
||||
) # at this point only roles based on specific access
|
||||
# First get the role based on specific access
|
||||
role = self.get_role(user)
|
||||
|
||||
# Characteristics that are based only on specific access
|
||||
is_owner = RoleChoices.OWNER in roles
|
||||
is_owner = role == RoleChoices.OWNER
|
||||
is_deleted = self.ancestors_deleted_at and not is_owner
|
||||
is_owner_or_admin = (is_owner or RoleChoices.ADMIN in roles) and not is_deleted
|
||||
is_owner_or_admin = (is_owner or role == RoleChoices.ADMIN) and not is_deleted
|
||||
|
||||
# Compute access roles before adding link roles because we don't
|
||||
# want anonymous users to access versions (we wouldn't know from
|
||||
# which date to allow them anyway)
|
||||
# Anonymous users should also not see document accesses
|
||||
has_access_role = bool(roles) and not is_deleted
|
||||
has_access_role = bool(role) and not is_deleted
|
||||
can_update_from_access = (
|
||||
is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
) and not is_deleted
|
||||
|
||||
# Add roles provided by the document link, taking into account its ancestors
|
||||
links_definitions = self.get_links_definitions(ancestors_links)
|
||||
public_roles = links_definitions.get(LinkReachChoices.PUBLIC, set())
|
||||
authenticated_roles = (
|
||||
links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
|
||||
if user.is_authenticated
|
||||
else set()
|
||||
link_select_options = LinkReachChoices.get_select_options(
|
||||
**self.ancestors_link_definition
|
||||
)
|
||||
link_definition = get_equivalent_link_definition(
|
||||
[
|
||||
self.ancestors_link_definition,
|
||||
{"link_reach": self.link_reach, "link_role": self.link_role},
|
||||
]
|
||||
)
|
||||
roles = roles | public_roles | authenticated_roles
|
||||
|
||||
can_get = bool(roles) and not is_deleted
|
||||
link_reach = link_definition["link_reach"]
|
||||
if link_reach == LinkReachChoices.PUBLIC or (
|
||||
link_reach == LinkReachChoices.AUTHENTICATED and user.is_authenticated
|
||||
):
|
||||
role = RoleChoices.max(role, link_definition["link_role"])
|
||||
|
||||
can_get = bool(role) and not is_deleted
|
||||
can_update = (
|
||||
is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
) and not is_deleted
|
||||
|
||||
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
||||
@@ -851,7 +790,7 @@ class Document(MP_Node, BaseModel):
|
||||
"restore": is_owner,
|
||||
"retrieve": can_get,
|
||||
"media_auth": can_get,
|
||||
"link_select_options": LinkReachChoices.get_select_options(ancestors_links),
|
||||
"link_select_options": link_select_options,
|
||||
"tree": can_get,
|
||||
"update": can_update,
|
||||
"versions_destroy": is_owner_or_admin,
|
||||
@@ -1098,54 +1037,133 @@ class DocumentAccess(BaseAccess):
|
||||
def __str__(self):
|
||||
return f"{self.user!s} is {self.role:s} in document {self.document!s}"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""initialize cache attribute."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._prefetched_user_roles_tuple = None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Override save to clear the document's cache for number of accesses."""
|
||||
super().save(*args, **kwargs)
|
||||
self.document.invalidate_nb_accesses_cache()
|
||||
|
||||
@property
|
||||
def target_key(self):
|
||||
"""Get a unique key for the actor targeted by the access, without possible conflict."""
|
||||
return f"user:{self.user_id!s}" if self.user_id else f"team:{self.team:s}"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Override delete to clear the document's cache for number of accesses."""
|
||||
super().delete(*args, **kwargs)
|
||||
self.document.invalidate_nb_accesses_cache()
|
||||
|
||||
def set_user_roles_tuple(self, ancestors_role, current_role):
|
||||
"""
|
||||
Set a precomputed (ancestor_role, current_role) tuple for this instance.
|
||||
|
||||
This avoids querying the database in `get_roles_tuple()` and is useful
|
||||
when roles are already known, such as in bulk serialization.
|
||||
|
||||
Args:
|
||||
ancestor_role (str | None): Highest role on any ancestor document.
|
||||
current_role (str | None): Role on the current document.
|
||||
"""
|
||||
self._prefetched_user_roles_tuple = (ancestors_role, current_role)
|
||||
|
||||
def get_user_roles_tuple(self, user):
|
||||
"""
|
||||
Return a tuple of:
|
||||
- the highest role the user has on any ancestor of the document
|
||||
- the role the user has on the current document
|
||||
|
||||
If roles have been explicitly set using `set_user_roles_tuple()`,
|
||||
those will be returned instead of querying the database.
|
||||
|
||||
This allows viewsets or serializers to precompute roles for performance
|
||||
when handling multiple documents at once.
|
||||
|
||||
Args:
|
||||
user (User): The user whose roles are being evaluated.
|
||||
|
||||
Returns:
|
||||
tuple[str | None, str | None]: (max_ancestor_role, current_document_role)
|
||||
"""
|
||||
if not user.is_authenticated:
|
||||
return None, None
|
||||
|
||||
if self._prefetched_user_roles_tuple is not None:
|
||||
return self._prefetched_user_roles_tuple
|
||||
|
||||
ancestors = (
|
||||
self.document.get_ancestors() | Document.objects.filter(pk=self.document_id)
|
||||
).filter(ancestors_deleted_at__isnull=True)
|
||||
|
||||
access_tuples = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__in=ancestors,
|
||||
).values_list("document_id", "role")
|
||||
|
||||
ancestors_roles = []
|
||||
current_roles = []
|
||||
for doc_id, role in access_tuples:
|
||||
if doc_id == self.document_id:
|
||||
current_roles.append(role)
|
||||
else:
|
||||
ancestors_roles.append(role)
|
||||
|
||||
return RoleChoices.max(*ancestors_roles), RoleChoices.max(*current_roles)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the document access.
|
||||
"""
|
||||
roles = self._get_roles(self.document, user)
|
||||
is_owner_or_admin = bool(set(roles).intersection(set(PRIVILEGED_ROLES)))
|
||||
ancestors_role, current_role = self.get_user_roles_tuple(user)
|
||||
role = RoleChoices.max(ancestors_role, current_role)
|
||||
is_owner_or_admin = role in PRIVILEGED_ROLES
|
||||
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (
|
||||
RoleChoices.OWNER in roles
|
||||
and self.document.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||
)
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
can_delete = role == RoleChoices.OWNER and (
|
||||
# check if document is not root trying to avoid an extra query
|
||||
self.document.depth > 1
|
||||
or DocumentAccess.objects.filter(
|
||||
document_id=self.document_id, role=RoleChoices.OWNER
|
||||
).count()
|
||||
> 1
|
||||
)
|
||||
set_role_to = RoleChoices.values if can_delete else []
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if RoleChoices.OWNER in roles:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
[RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN]
|
||||
)
|
||||
if role == RoleChoices.OWNER:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
# Filter out roles that would be lower than the one the user already has
|
||||
ancestors_role_priority = RoleChoices.get_priority(
|
||||
getattr(self, "max_ancestors_role", None)
|
||||
)
|
||||
set_role_to = [
|
||||
candidate_role
|
||||
for candidate_role in set_role_to
|
||||
if RoleChoices.get_priority(candidate_role) > ancestors_role_priority
|
||||
]
|
||||
child_set_role_to = [
|
||||
candidate_role
|
||||
for candidate_role in set_role_to
|
||||
if RoleChoices.get_priority(candidate_role)
|
||||
> RoleChoices.get_priority(self.role)
|
||||
]
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to) and is_owner_or_admin,
|
||||
"partial_update": bool(set_role_to) and is_owner_or_admin,
|
||||
"retrieve": self.user and self.user.id == user.id or is_owner_or_admin,
|
||||
"retrieve": (self.user and self.user.id == user.id) or is_owner_or_admin,
|
||||
"set_role_to": set_role_to,
|
||||
"child_set_role_to": child_set_role_to,
|
||||
}
|
||||
|
||||
|
||||
@@ -1277,10 +1295,10 @@ class Template(BaseModel):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_roles(self, user):
|
||||
def get_role(self, user):
|
||||
"""Return the roles a user has on a resource as an iterable."""
|
||||
if not user.is_authenticated:
|
||||
return []
|
||||
return None
|
||||
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
@@ -1291,21 +1309,20 @@ class Template(BaseModel):
|
||||
).values_list("role", flat=True)
|
||||
except (models.ObjectDoesNotExist, IndexError):
|
||||
roles = []
|
||||
return roles
|
||||
|
||||
return RoleChoices.max(*roles)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the template.
|
||||
"""
|
||||
roles = self.get_roles(user)
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
can_get = self.is_public or bool(roles)
|
||||
can_update = is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||
role = self.get_role(user)
|
||||
is_owner_or_admin = role in PRIVILEGED_ROLES
|
||||
can_get = self.is_public or bool(role)
|
||||
can_update = is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
|
||||
return {
|
||||
"destroy": RoleChoices.OWNER in roles,
|
||||
"destroy": role == RoleChoices.OWNER,
|
||||
"generate_document": can_get,
|
||||
"accesses_manage": is_owner_or_admin,
|
||||
"update": can_update,
|
||||
@@ -1352,11 +1369,65 @@ class TemplateAccess(BaseAccess):
|
||||
def __str__(self):
|
||||
return f"{self.user!s} is {self.role:s} in template {self.template!s}"
|
||||
|
||||
def get_role(self, user):
|
||||
"""
|
||||
Get the role a user has on a resource.
|
||||
"""
|
||||
if not user.is_authenticated:
|
||||
return None
|
||||
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
except AttributeError:
|
||||
teams = user.teams
|
||||
try:
|
||||
roles = self.template.accesses.filter(
|
||||
models.Q(user=user) | models.Q(team__in=teams),
|
||||
).values_list("role", flat=True)
|
||||
except (Template.DoesNotExist, IndexError):
|
||||
roles = []
|
||||
|
||||
return RoleChoices.max(*roles)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the template access.
|
||||
"""
|
||||
return self._get_abilities(self.template, user)
|
||||
role = self.get_role(user)
|
||||
is_owner_or_admin = role in PRIVILEGED_ROLES
|
||||
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (role == RoleChoices.OWNER) and self.template.accesses.filter(
|
||||
role=RoleChoices.OWNER
|
||||
).count() > 1
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
)
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if role == RoleChoices.OWNER:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to),
|
||||
"partial_update": bool(set_role_to),
|
||||
"retrieve": bool(role),
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
|
||||
class Invitation(BaseModel):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ from django.core import mail
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core import choices, factories, models
|
||||
from core.api import serializers
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_api_document_accesses_create_anonymous():
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"document": str(document.id),
|
||||
"role": random.choice(models.RoleChoices.values),
|
||||
"role": random.choice(choices.RoleChoices.values),
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -88,7 +88,7 @@ def test_api_document_accesses_create_authenticated_reader_or_editor(
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
for new_role in [role[0] for role in models.RoleChoices.choices]:
|
||||
for new_role in choices.RoleChoices.values:
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
@@ -103,32 +103,37 @@ def test_api_document_accesses_create_authenticated_reader_or_editor(
|
||||
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_administrator(via, mock_user_teams):
|
||||
def test_api_document_accesses_create_authenticated_administrator_share_to_user(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Administrators of a document should be able to create document accesses
|
||||
except for the "owner" role.
|
||||
Administrators of a document (direct or by heritage) should be able to create
|
||||
document accesses for a user except for the "owner" role.
|
||||
An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="administrator"
|
||||
document=documents[0], user=user, role="administrator"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="administrator"
|
||||
document=documents[0], team="lasuite", role="administrator"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
|
||||
# It should not be allowed to create an owner access
|
||||
document = documents[-1]
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
@@ -140,12 +145,12 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Only owners of a resource can assign other users as owners."
|
||||
"detail": "Only owners of a document can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
role = random.choice(
|
||||
[role[0] for role in models.RoleChoices.choices if role[0] != "owner"]
|
||||
[role for role in choices.RoleChoices.values if role != "owner"]
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
@@ -165,9 +170,16 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"depth": new_document_access.document.depth,
|
||||
"path": new_document_access.document.path,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"team": "",
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "",
|
||||
"user": other_user,
|
||||
}
|
||||
assert len(mail.outbox) == 1
|
||||
@@ -182,29 +194,121 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
def test_api_document_accesses_create_authenticated_administrator_share_to_team(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Owners of a document should be able to create document accesses whatever the role.
|
||||
Administrators of a document (direct or by heritage) should be able to create
|
||||
document accesses for a team except for the "owner" role.
|
||||
An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role="administrator"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=documents[0], team="lasuite", role="administrator"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
document = documents[-1]
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "new-team",
|
||||
"role": "owner",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Only owners of a document can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
role = random.choice(
|
||||
[role for role in choices.RoleChoices.values if role != "owner"]
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "new-team",
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(team="new-team").count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(team="new-team").get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"depth": new_document_access.document.depth,
|
||||
"path": new_document_access.document.path,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "new-team",
|
||||
"user": None,
|
||||
}
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner_share_to_user(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Owners of a document (direct or by heritage) should be able to create document accesses
|
||||
for a user, whatever the role. An email should be sent to the accesses to notify them
|
||||
of the adding.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="owner"
|
||||
document=documents[0], team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
document = documents[-1]
|
||||
role = random.choice(choices.RoleChoices.values)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
@@ -222,11 +326,18 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"depth": new_document_access.document.depth,
|
||||
"path": new_document_access.document.path,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "",
|
||||
"user": other_user,
|
||||
}
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -240,6 +351,187 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner_share_to_team(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Owners of a document (direct or by heritage) should be able to create document accesses
|
||||
for a team whatever the role. An email should be sent to the accesses to notify them of
|
||||
the adding.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=documents[0], team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
document = documents[-1]
|
||||
role = random.choice(choices.RoleChoices.values)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "new-team",
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(team="new-team").count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(team="new-team").get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"path": new_document_access.document.path,
|
||||
"depth": new_document_access.document.depth,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "new-team",
|
||||
"user": None,
|
||||
}
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("override_role", choices.RoleChoices.values)
|
||||
@pytest.mark.parametrize("parent_role", choices.RoleChoices.values)
|
||||
def test_api_document_accesses_create_authenticated_higher_role_to_user(
|
||||
parent_role, override_role
|
||||
):
|
||||
"""
|
||||
It should not be allowed to create a document access override for a user
|
||||
with a role lower or equal to the inherited role.
|
||||
"""
|
||||
user, other_user = factories.UserFactory.create_batch(2)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(
|
||||
users=([user, "owner"], [other_user, parent_role])
|
||||
)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"role": override_role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
get_priority = choices.RoleChoices.get_priority
|
||||
if get_priority(override_role) > get_priority(parent_role):
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(user=other_user).count() == 2
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"role": [
|
||||
"Role overrides must be greater than the inherited role: "
|
||||
f"{parent_role}/{override_role}"
|
||||
],
|
||||
}
|
||||
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("override_role", choices.RoleChoices.values)
|
||||
@pytest.mark.parametrize("parent_role", choices.RoleChoices.values)
|
||||
def test_api_document_accesses_create_authenticated_higher_role_to_team(
|
||||
parent_role, override_role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
It should not be allowed to create a document access override for a team
|
||||
with a role lower or equal to the inherited role.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
|
||||
parent = factories.DocumentFactory(
|
||||
users=[[user, "owner"]], teams=[["lasuite", parent_role]]
|
||||
)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "lasuite",
|
||||
"role": override_role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
get_priority = choices.RoleChoices.get_priority
|
||||
if get_priority(override_role) > get_priority(parent_role):
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(team="lasuite").count() == 2
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"role": [
|
||||
"Role overrides must be greater than the inherited role: "
|
||||
f"{parent_role}/{override_role}"
|
||||
],
|
||||
}
|
||||
assert models.DocumentAccess.objects.filter(team="lasuite").count() == 1
|
||||
|
||||
|
||||
def test_api_document_accesses_create_authenticated_user_and_team():
|
||||
"""Trying to create a document access with a user and a team should return a 400 error."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[[user, "owner"]])
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"team": "lasuite",
|
||||
"role": "reader",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"__all__": ["Either user or team must be set, not both."]
|
||||
}
|
||||
assert models.DocumentAccess.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_email_in_receivers_language(via, mock_user_teams):
|
||||
"""
|
||||
@@ -259,7 +551,7 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
|
||||
document=document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
role = random.choice(choices.RoleChoices.values)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
@@ -286,11 +578,18 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
|
||||
).get()
|
||||
other_user_data = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user_data,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"path": new_document_access.document.path,
|
||||
"depth": new_document_access.document.depth,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "",
|
||||
"user": other_user_data,
|
||||
}
|
||||
assert len(mail.outbox) == index + 1
|
||||
email = mail.outbox[index]
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-positional-arguments, too-many-arguments
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core import mail
|
||||
from django.test import override_settings
|
||||
@@ -341,6 +343,7 @@ def test_api_document_invitations_create_authenticated_outsider():
|
||||
|
||||
|
||||
@override_settings(EMAIL_BRAND_NAME="My brand name", EMAIL_LOGO_IMG="my-img.jpg")
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize(
|
||||
"inviting,invited,response_code",
|
||||
(
|
||||
@@ -363,22 +366,31 @@ def test_api_document_invitations_create_authenticated_outsider():
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations_create_privileged_members(
|
||||
via, inviting, invited, response_code, mock_user_teams
|
||||
def test_api_document_invitations_create_privileged_members( # noqa: PLR0913
|
||||
via, inviting, invited, response_code, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Only owners and administrators should be able to invite new users.
|
||||
Only owners can invite owners.
|
||||
"""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=inviting)
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role=inviting
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=inviting
|
||||
document=documents[0], team="lasuite", role=inviting
|
||||
)
|
||||
document = documents[-1]
|
||||
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
@@ -422,6 +434,30 @@ def test_api_document_invitations_create_privileged_members(
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_invitations_create_unknown_document():
|
||||
"""Trying to create an invitation for an unknown document should return a 404."""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
"role": "reader",
|
||||
}
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{uuid4()!s}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert models.Invitation.objects.exists() is False
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_document_invitations_create_email_from_senders_language():
|
||||
"""
|
||||
When inviting on a document a user who does not exist yet in our database,
|
||||
|
||||
@@ -98,7 +98,9 @@ def test_api_documents_children_create_authenticated_success(reach, role, depth)
|
||||
if i == 0:
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||
else:
|
||||
document = factories.DocumentFactory(parent=document, link_role="reader")
|
||||
document = factories.DocumentFactory(
|
||||
parent=document, link_reach="restricted"
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
@@ -112,7 +114,8 @@ def test_api_documents_children_create_authenticated_success(reach, role, depth)
|
||||
child = Document.objects.get(id=response.json()["id"])
|
||||
assert child.title == "my child"
|
||||
assert child.link_reach == "restricted"
|
||||
assert child.accesses.filter(role="owner", user=user).exists()
|
||||
# Access objects on the child are not necessary
|
||||
assert child.accesses.exists() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@@ -180,7 +183,8 @@ def test_api_documents_children_create_related_success(role, depth):
|
||||
child = Document.objects.get(id=response.json()["id"])
|
||||
assert child.title == "my child"
|
||||
assert child.link_reach == "restricted"
|
||||
assert child.accesses.filter(role="owner", user=user).exists()
|
||||
# Access objects on the child are not necessary
|
||||
assert child.accesses.exists() is False
|
||||
|
||||
|
||||
def test_api_documents_children_create_authenticated_title_null():
|
||||
|
||||
@@ -14,13 +14,18 @@ from core import factories
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_children_list_anonymous_public_standalone():
|
||||
def test_api_documents_children_list_anonymous_public_standalone(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Anonymous users should be allowed to retrieve the children of a public document."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(8):
|
||||
APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(4):
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -30,6 +35,10 @@ def test_api_documents_children_list_anonymous_public_standalone():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -44,10 +53,14 @@ def test_api_documents_children_list_anonymous_public_standalone():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -62,13 +75,13 @@ def test_api_documents_children_list_anonymous_public_standalone():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_anonymous_public_parent():
|
||||
def test_api_documents_children_list_anonymous_public_parent(django_assert_num_queries):
|
||||
"""
|
||||
Anonymous users should be allowed to retrieve the children of a document who
|
||||
has a public ancestor.
|
||||
@@ -83,7 +96,10 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(9):
|
||||
APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(5):
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -93,6 +109,10 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -107,10 +127,14 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -125,7 +149,7 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -149,7 +173,7 @@ def test_api_documents_children_list_anonymous_restricted_or_authenticated(reach
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_children_list_authenticated_unrelated_public_or_authenticated(
|
||||
reach,
|
||||
reach, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to retrieve the children of a public/authenticated
|
||||
@@ -163,9 +187,13 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(9):
|
||||
client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(5):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 2,
|
||||
@@ -174,6 +202,10 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -188,10 +220,14 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -206,7 +242,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -214,7 +250,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_children_list_authenticated_public_or_authenticated_parent(
|
||||
reach,
|
||||
reach, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document who
|
||||
@@ -231,7 +267,11 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(10):
|
||||
client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
with django_assert_num_queries(6):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -241,6 +281,10 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -255,10 +299,14 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -273,13 +321,15 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_unrelated_restricted():
|
||||
def test_api_documents_children_list_authenticated_unrelated_restricted(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve the children of a document that is
|
||||
restricted and to which they are not related.
|
||||
@@ -293,16 +343,20 @@ def test_api_documents_children_list_authenticated_unrelated_restricted():
|
||||
child1, _child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(2):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_direct():
|
||||
def test_api_documents_children_list_authenticated_related_direct(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document
|
||||
to which they are directly related whatever the role.
|
||||
@@ -319,10 +373,13 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(9):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
link_role = None if document.link_reach == "restricted" else document.link_role
|
||||
assert response.json() == {
|
||||
"count": 2,
|
||||
"next": None,
|
||||
@@ -330,6 +387,10 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": document.link_reach,
|
||||
"ancestors_link_role": link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -344,10 +405,14 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": document.link_reach,
|
||||
"ancestors_link_role": link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -362,13 +427,15 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_parent():
|
||||
def test_api_documents_children_list_authenticated_related_parent(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document if they
|
||||
are related to one of its ancestors whatever the role.
|
||||
@@ -389,9 +456,11 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
document=grand_parent, user=user
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(10):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 2,
|
||||
@@ -400,6 +469,10 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -414,10 +487,14 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -432,13 +509,15 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_child():
|
||||
def test_api_documents_children_list_authenticated_related_child(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve all the children of a document
|
||||
as a result of being related to one of its children.
|
||||
@@ -454,16 +533,20 @@ def test_api_documents_children_list_authenticated_related_child():
|
||||
factories.UserDocumentAccessFactory(document=child1, user=user)
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(2):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_team_none(mock_user_teams):
|
||||
def test_api_documents_children_list_authenticated_related_team_none(
|
||||
mock_user_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should not be able to retrieve the children of a restricted document
|
||||
related to teams in which the user is not.
|
||||
@@ -480,7 +563,9 @@ def test_api_documents_children_list_authenticated_related_team_none(mock_user_t
|
||||
|
||||
factories.TeamDocumentAccessFactory(document=document, team="myteam")
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(2):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
@@ -488,7 +573,7 @@ def test_api_documents_children_list_authenticated_related_team_none(mock_user_t
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_team_members(
|
||||
mock_user_teams,
|
||||
mock_user_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document to which they
|
||||
@@ -506,7 +591,8 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
|
||||
access = factories.TeamDocumentAccessFactory(document=document, team="myteam")
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(9):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
# pylint: disable=R0801
|
||||
assert response.status_code == 200
|
||||
@@ -517,6 +603,10 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -531,10 +621,14 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -549,7 +643,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -46,10 +50,16 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": "editor"
|
||||
if (child1.link_reach == "public" and child1.link_role == "editor")
|
||||
else document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 3,
|
||||
@@ -64,10 +74,14 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -82,7 +96,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -115,6 +129,10 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -129,10 +147,14 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 5,
|
||||
@@ -147,10 +169,14 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -165,7 +191,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -201,7 +227,9 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(
|
||||
2, parent=document, link_reach="restricted"
|
||||
)
|
||||
grand_child = factories.DocumentFactory(parent=child1)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
@@ -217,6 +245,10 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -231,10 +263,14 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 3,
|
||||
@@ -249,10 +285,14 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -267,7 +307,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -289,7 +329,9 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
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)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(
|
||||
2, parent=document, link_reach="restricted"
|
||||
)
|
||||
grand_child = factories.DocumentFactory(parent=child1)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
@@ -304,6 +346,10 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -318,10 +364,14 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 5,
|
||||
@@ -336,10 +386,14 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -354,7 +408,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -414,6 +468,10 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -428,10 +486,14 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": grand_child.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 3,
|
||||
@@ -446,10 +508,14 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -464,7 +530,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -504,6 +570,10 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -518,10 +588,14 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": grand_child.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 5,
|
||||
@@ -536,10 +610,14 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"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],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -554,7 +632,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -640,6 +718,10 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -654,10 +736,14 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": grand_child.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"depth": 3,
|
||||
@@ -672,10 +758,14 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -690,7 +780,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils import timezone
|
||||
import pycrdt
|
||||
import pytest
|
||||
import requests
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
@@ -133,19 +134,21 @@ def test_api_documents_duplicate_success(index):
|
||||
|
||||
# Ensure access persists after the owner loses access to the original document
|
||||
models.DocumentAccess.objects.filter(document=document).delete()
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=image_refs[0][1]
|
||||
)
|
||||
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=image_refs[0][1]
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
authorization = response["Authorization"]
|
||||
assert "AWS4-HMAC-SHA256 Credential=" in authorization
|
||||
assert (
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
response = requests.get(
|
||||
|
||||
@@ -59,6 +59,10 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
||||
"results": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"content": document.content,
|
||||
@@ -74,7 +78,7 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": ["reader"],
|
||||
"user_role": "reader",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ def test_api_documents_list_format():
|
||||
assert results[0] == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
@@ -76,7 +80,7 @@ def test_api_documents_list_format():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -148,11 +152,11 @@ def test_api_documents_list_authenticated_direct(django_assert_num_queries):
|
||||
str(child4_with_access.id),
|
||||
}
|
||||
|
||||
with django_assert_num_queries(12):
|
||||
with django_assert_num_queries(14):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
# nb_accesses should now be cached
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(6):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -268,11 +272,11 @@ def test_api_documents_list_authenticated_link_reach_public_or_authenticated(
|
||||
|
||||
expected_ids = {str(document1.id), str(document2.id), str(visible_child.id)}
|
||||
|
||||
with django_assert_num_queries(10):
|
||||
with django_assert_num_queries(11):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
# nb_accesses should now be cached
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(5):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
@@ -52,9 +53,11 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -64,7 +67,7 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -167,9 +170,11 @@ def test_api_documents_media_auth_anonymous_attachments():
|
||||
parent = factories.DocumentFactory(link_reach="public")
|
||||
factories.DocumentFactory(parent=parent, link_reach="restricted", attachments=[key])
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -179,7 +184,7 @@ def test_api_documents_media_auth_anonymous_attachments():
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -221,9 +226,11 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach=reach, attachments=[key])
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -233,7 +240,7 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -307,9 +314,11 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -319,7 +328,7 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -373,10 +382,12 @@ def test_api_documents_media_auth_missing_status_metadata():
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
now = timezone.now()
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -386,7 +397,7 @@ def test_api_documents_media_auth_missing_status_metadata():
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
|
||||
@@ -124,8 +124,8 @@ def test_api_documents_move_authenticated_target_roles_mocked(
|
||||
target_role, target_parent_role, position
|
||||
):
|
||||
"""
|
||||
Authenticated users with insufficient permissions on the target document (or its
|
||||
parent depending on the position chosen), should not be allowed to move documents.
|
||||
Only authenticated users with sufficient permissions on the target document (or its
|
||||
parent depending on the position chosen), should be allowed to move documents.
|
||||
"""
|
||||
|
||||
user = factories.UserFactory()
|
||||
@@ -208,6 +208,107 @@ def test_api_documents_move_authenticated_target_roles_mocked(
|
||||
assert document.is_root() is True
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_no_owner_user_and_team():
|
||||
"""
|
||||
Moving a document with no owner to the root of the tree should automatically declare
|
||||
the owner of the previous root of the document as owner of the document itself.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent_owner = factories.UserFactory()
|
||||
parent = factories.DocumentFactory(
|
||||
users=[(parent_owner, "owner")], teams=[("lasuite", "owner")]
|
||||
)
|
||||
# A document with no owner
|
||||
document = factories.DocumentFactory(parent=parent, users=[(user, "administrator")])
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": "first-sibling"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
assert list(target.get_siblings()) == [document, parent, target]
|
||||
|
||||
document.refresh_from_db()
|
||||
assert list(document.get_children()) == [child]
|
||||
assert document.accesses.count() == 3
|
||||
assert document.accesses.get(user__isnull=False, role="owner").user == parent_owner
|
||||
assert document.accesses.get(user__isnull=True, role="owner").team == "lasuite"
|
||||
assert document.accesses.get(role="administrator").user == user
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_no_owner_same_user():
|
||||
"""
|
||||
Moving a document should not fail if the user moving a document with no owner was
|
||||
at the same time owner of the previous root and has a role on the document being moved.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(
|
||||
users=[(user, "owner")], teams=[("lasuite", "owner")]
|
||||
)
|
||||
# A document with no owner
|
||||
document = factories.DocumentFactory(parent=parent, users=[(user, "reader")])
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": "first-sibling"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
assert list(target.get_siblings()) == [document, parent, target]
|
||||
|
||||
document.refresh_from_db()
|
||||
assert list(document.get_children()) == [child]
|
||||
assert document.accesses.count() == 2
|
||||
assert document.accesses.get(user__isnull=False, role="owner").user == user
|
||||
assert document.accesses.get(user__isnull=True, role="owner").team == "lasuite"
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_no_owner_same_team():
|
||||
"""
|
||||
Moving a document should not fail if the team that is owner of the document root was
|
||||
already declared on the document with a different role.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(teams=[("lasuite", "owner")])
|
||||
# A document with no owner but same team
|
||||
document = factories.DocumentFactory(
|
||||
parent=parent, users=[(user, "administrator")], teams=[("lasuite", "reader")]
|
||||
)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": "first-sibling"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
assert list(target.get_siblings()) == [document, parent, target]
|
||||
|
||||
document.refresh_from_db()
|
||||
assert list(document.get_children()) == [child]
|
||||
assert document.accesses.count() == 2
|
||||
assert document.accesses.get(user__isnull=False, role="administrator").user == user
|
||||
assert document.accesses.get(user__isnull=True, role="owner").team == "lasuite"
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_deleted_document():
|
||||
"""
|
||||
It should not be possible to move a deleted document or its descendants, even
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: retrieve
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import random
|
||||
from datetime import timedelta
|
||||
@@ -11,7 +12,7 @@ from django.utils import timezone
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core import choices, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -45,7 +46,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -59,6 +60,10 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -73,7 +78,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +96,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
links_definition = choices.get_equivalent_link_definition(links)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -110,7 +116,9 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(links),
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(
|
||||
**links_definition
|
||||
),
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -123,6 +131,10 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": grand_parent.link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -137,7 +149,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +221,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -223,6 +235,10 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -237,7 +253,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
|
||||
@@ -263,6 +279,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
links_definition = choices.get_equivalent_link_definition(links)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -281,7 +298,9 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(links),
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(
|
||||
**links_definition
|
||||
),
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -294,6 +313,10 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -308,7 +331,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +427,10 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"creator": str(document.creator.id),
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -418,7 +445,7 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +471,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
)
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
link_definition = choices.get_equivalent_link_definition(links)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -462,7 +490,9 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"favorite": True,
|
||||
"invite_owner": access.role == "owner",
|
||||
"link_configuration": access.role in ["administrator", "owner"],
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(links),
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(
|
||||
**link_definition
|
||||
),
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": access.role in ["administrator", "owner"],
|
||||
@@ -475,6 +505,10 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
},
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": "restricted",
|
||||
"computed_link_role": None,
|
||||
"content": document.content,
|
||||
"creator": str(document.creator.id),
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -489,7 +523,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -585,16 +619,16 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_teams)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"teams,roles",
|
||||
"teams,role",
|
||||
[
|
||||
[["readers"], ["reader"]],
|
||||
[["unknown", "readers"], ["reader"]],
|
||||
[["editors"], ["editor"]],
|
||||
[["unknown", "editors"], ["editor"]],
|
||||
[["readers"], "reader"],
|
||||
[["unknown", "readers"], "reader"],
|
||||
[["editors"], "editor"],
|
||||
[["unknown", "editors"], "editor"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
teams, roles, mock_user_teams
|
||||
teams, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a document to which they
|
||||
@@ -627,6 +661,10 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -641,20 +679,20 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": roles,
|
||||
"user_role": role,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"teams,roles",
|
||||
"teams,role",
|
||||
[
|
||||
[["administrators"], ["administrator"]],
|
||||
[["editors", "administrators"], ["administrator", "editor"]],
|
||||
[["unknown", "administrators"], ["administrator"]],
|
||||
[["administrators"], "administrator"],
|
||||
[["editors", "administrators"], "administrator"],
|
||||
[["unknown", "administrators"], "administrator"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
teams, roles, mock_user_teams
|
||||
teams, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a document to which they
|
||||
@@ -689,6 +727,10 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -703,21 +745,21 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": roles,
|
||||
"user_role": role,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"teams,roles",
|
||||
"teams,role",
|
||||
[
|
||||
[["owners"], ["owner"]],
|
||||
[["owners", "administrators"], ["owner", "administrator"]],
|
||||
[["members", "administrators", "owners"], ["owner", "administrator"]],
|
||||
[["unknown", "owners"], ["owner"]],
|
||||
[["owners"], "owner"],
|
||||
[["owners", "administrators"], "owner"],
|
||||
[["members", "administrators", "owners"], "owner"],
|
||||
[["unknown", "owners"], "owner"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
teams, roles, mock_user_teams
|
||||
teams, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a restricted document to which
|
||||
@@ -751,6 +793,10 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -765,11 +811,11 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": roles,
|
||||
"user_role": role,
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
|
||||
def test_api_documents_retrieve_user_role(django_assert_max_num_queries):
|
||||
"""
|
||||
Roles should be annotated on querysets taking into account all documents ancestors.
|
||||
"""
|
||||
@@ -792,15 +838,14 @@ def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
|
||||
factories.UserDocumentAccessFactory(document=parent, user=user),
|
||||
factories.UserDocumentAccessFactory(document=document, user=user),
|
||||
)
|
||||
expected_roles = {access.role for access in accesses}
|
||||
expected_role = choices.RoleChoices.max(*[access.role for access in accesses])
|
||||
|
||||
with django_assert_max_num_queries(14):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
user_roles = response.json()["user_roles"]
|
||||
assert set(user_roles) == expected_roles
|
||||
assert response.json()["user_role"] == expected_role
|
||||
|
||||
|
||||
def test_api_documents_retrieve_numqueries_with_link_trace(django_assert_num_queries):
|
||||
|
||||
@@ -88,7 +88,7 @@ def test_api_documents_trashbin_format():
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -102,6 +102,10 @@ def test_api_documents_trashbin_format():
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
@@ -114,7 +118,7 @@ def test_api_documents_trashbin_format():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": ["owner"],
|
||||
"user_role": "owner",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -32,13 +32,19 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(AnonymousUser()),
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -57,9 +63,13 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -74,11 +84,15 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": sibling1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": sibling1.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling1.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling1.computed_link_reach,
|
||||
"computed_link_role": sibling1.computed_link_role,
|
||||
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -93,11 +107,15 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": sibling1.path,
|
||||
"title": sibling1.title,
|
||||
"updated_at": sibling1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": sibling2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": sibling2.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling2.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling2.computed_link_reach,
|
||||
"computed_link_role": sibling2.computed_link_role,
|
||||
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -112,9 +130,11 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": sibling2.path,
|
||||
"title": sibling2.title,
|
||||
"updated_at": sibling2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -129,7 +149,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -163,18 +183,28 @@ 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() == {
|
||||
expected_tree = {
|
||||
"abilities": grand_parent.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": grand_parent.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": parent.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -193,9 +223,11 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -214,11 +246,15 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": document.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": document_sibling.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": document_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": document_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": document_sibling.computed_link_reach,
|
||||
"computed_link_role": document_sibling.computed_link_role,
|
||||
"created_at": document_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -237,9 +273,11 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": document_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
@@ -254,11 +292,15 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": parent_sibling.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": parent_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": parent_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": parent_sibling.computed_link_reach,
|
||||
"computed_link_role": parent_sibling.computed_link_role,
|
||||
"created_at": parent_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -277,9 +319,11 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": parent_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": grand_parent.computed_link_reach,
|
||||
"computed_link_role": grand_parent.computed_link_role,
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
@@ -294,8 +338,9 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"path": grand_parent.path,
|
||||
"title": grand_parent.title,
|
||||
"updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
assert response.json() == expected_tree
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["restricted", "authenticated"])
|
||||
@@ -341,13 +386,21 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -366,9 +419,11 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -383,11 +438,15 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": sibling.get_abilities(user),
|
||||
"ancestors_link_reach": sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling.computed_link_reach,
|
||||
"computed_link_role": sibling.computed_link_role,
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
@@ -402,9 +461,11 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"path": sibling.path,
|
||||
"title": sibling.title,
|
||||
"updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -419,7 +480,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -460,16 +521,26 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": grand_parent.get_abilities(user),
|
||||
"ancestors_link_reach": grand_parent.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -488,9 +559,11 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -509,11 +582,15 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": document.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": document_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": document_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": document_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": document_sibling.computed_link_reach,
|
||||
"computed_link_role": document_sibling.computed_link_role,
|
||||
"created_at": document_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -532,9 +609,11 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": document_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
@@ -549,11 +628,15 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": parent_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": parent_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": parent_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": parent_sibling.computed_link_reach,
|
||||
"computed_link_role": parent_sibling.computed_link_role,
|
||||
"created_at": parent_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -572,9 +655,11 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": parent_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": grand_parent.computed_link_reach,
|
||||
"computed_link_role": grand_parent.computed_link_role,
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
@@ -589,7 +674,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"path": grand_parent.path,
|
||||
"title": grand_parent.title,
|
||||
"updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -639,13 +724,21 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -664,9 +757,11 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -681,11 +776,15 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": sibling.get_abilities(user),
|
||||
"ancestors_link_reach": sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling.computed_link_reach,
|
||||
"computed_link_role": sibling.computed_link_role,
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
@@ -700,9 +799,11 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"path": sibling.path,
|
||||
"title": sibling.title,
|
||||
"updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -717,7 +818,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -762,16 +863,26 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": grand_parent.get_abilities(user),
|
||||
"ancestors_link_reach": grand_parent.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"children": [],
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -790,9 +901,11 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -811,11 +924,15 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": document.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": document_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": document_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": document_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": document_sibling.computed_link_reach,
|
||||
"computed_link_role": document_sibling.computed_link_role,
|
||||
"created_at": document_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -834,9 +951,11 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": document_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
@@ -851,11 +970,15 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": parent_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": parent_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": parent_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": parent_sibling.computed_link_reach,
|
||||
"computed_link_role": parent_sibling.computed_link_role,
|
||||
"created_at": parent_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -874,9 +997,11 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": parent_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": grand_parent.computed_link_reach,
|
||||
"computed_link_role": grand_parent.computed_link_role,
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
@@ -891,7 +1016,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"path": grand_parent.path,
|
||||
"title": grand_parent.title,
|
||||
"updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -949,13 +1074,21 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -974,9 +1107,11 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -991,11 +1126,15 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": sibling.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling.computed_link_reach,
|
||||
"computed_link_role": sibling.computed_link_role,
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
@@ -1010,9 +1149,11 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"path": sibling.path,
|
||||
"title": sibling.title,
|
||||
"updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -1027,5 +1168,5 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
@@ -155,6 +155,10 @@ def test_api_documents_update_anonymous_or_authenticated_unrelated(
|
||||
for key, value in document_values.items():
|
||||
if key in [
|
||||
"id",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"accesses",
|
||||
"created_at",
|
||||
"creator",
|
||||
@@ -270,6 +274,10 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
for key, value in document_values.items():
|
||||
if key in [
|
||||
"id",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
|
||||
@@ -48,12 +48,7 @@ def test_api_template_accesses_list_authenticated_unrelated():
|
||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -96,8 +91,8 @@ def test_api_template_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 3
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
assert len(content) == 3
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(user_access.id),
|
||||
|
||||
@@ -133,7 +133,7 @@ def test_api_template_accesses_create_authenticated_administrator(via, mock_user
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Only owners of a resource can assign other users as owners."
|
||||
"detail": "Only owners of a template can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
|
||||
@@ -186,7 +186,7 @@ def test_api_users_list_query_short_queries():
|
||||
"""
|
||||
Queries shorter than 5 characters should return an empty result set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
user = factories.UserFactory(email="paul@example.com")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ def test_models_document_access_get_abilities_anonymous():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +105,7 @@ def test_models_document_access_get_abilities_authenticated():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -123,22 +125,53 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_self_last():
|
||||
def test_models_document_access_get_abilities_for_owner_of_self_last_on_root(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Check abilities of self access for the owner of a document when there is only one owner left.
|
||||
Check abilities of self access for the owner of a root document when there
|
||||
is only one owner left.
|
||||
"""
|
||||
access = factories.UserDocumentAccessFactory(role="owner")
|
||||
abilities = access.get_abilities(access.user)
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
abilities = access.get_abilities(access.user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_self_last_on_child(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Check abilities of self access for the owner of a child document when there
|
||||
is only one owner left.
|
||||
"""
|
||||
parent = factories.DocumentFactory()
|
||||
access = factories.UserDocumentAccessFactory(document__parent=parent, role="owner")
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.get_abilities(access.user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +188,8 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +206,8 @@ def test_models_document_access_get_abilities_for_owner_of_administrator():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
"child_set_role_to": ["owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +224,8 @@ def test_models_document_access_get_abilities_for_owner_of_editor():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
"child_set_role_to": ["administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +242,8 @@ def test_models_document_access_get_abilities_for_owner_of_reader():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "editor"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
"child_set_role_to": ["editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +264,7 @@ def test_models_document_access_get_abilities_for_administrator_of_owner():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +281,8 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator"],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +299,8 @@ def test_models_document_access_get_abilities_for_administrator_of_editor():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator"],
|
||||
"child_set_role_to": ["administrator"],
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +317,8 @@ def test_models_document_access_get_abilities_for_administrator_of_reader():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor"],
|
||||
"set_role_to": ["reader", "editor", "administrator"],
|
||||
"child_set_role_to": ["editor", "administrator"],
|
||||
}
|
||||
|
||||
|
||||
@@ -298,6 +339,7 @@ def test_models_document_access_get_abilities_for_editor_of_owner():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -315,6 +357,7 @@ def test_models_document_access_get_abilities_for_editor_of_administrator():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +380,7 @@ def test_models_document_access_get_abilities_for_editor_of_editor_user(
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -357,6 +401,7 @@ def test_models_document_access_get_abilities_for_reader_of_owner():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -374,6 +419,7 @@ def test_models_document_access_get_abilities_for_reader_of_administrator():
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -396,16 +442,17 @@ def test_models_document_access_get_abilities_for_reader_of_reader_user(
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_preset_role(django_assert_num_queries):
|
||||
"""No query is done if the role is preset, e.g., with a query annotation."""
|
||||
"""No query is done if user roles are preset on the document, e.g., with a query annotation."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="reader"
|
||||
).user
|
||||
access.user_roles = ["reader"]
|
||||
access.set_user_roles_tuple(None, "reader")
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
abilities = access.get_abilities(user)
|
||||
@@ -416,6 +463,7 @@ def test_models_document_access_get_abilities_preset_role(django_assert_num_quer
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
"child_set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"partial_update": False,
|
||||
"restore": False,
|
||||
@@ -229,7 +229,7 @@ def test_models_documents_get_abilities_reader(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -252,7 +252,7 @@ def test_models_documents_get_abilities_reader(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ def test_models_documents_get_abilities_editor(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -314,7 +314,7 @@ def test_models_documents_get_abilities_editor(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -344,7 +344,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -393,7 +393,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -415,7 +415,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -467,7 +467,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -504,7 +504,7 @@ def test_models_documents_get_abilities_reader_user(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -528,7 +528,7 @@ def test_models_documents_get_abilities_reader_user(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -561,7 +561,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
@@ -1176,184 +1176,134 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ancestors_links, select_options",
|
||||
"reach, role, select_options",
|
||||
[
|
||||
# One ancestor
|
||||
(
|
||||
[{"link_reach": "public", "link_role": "reader"}],
|
||||
"public",
|
||||
"reader",
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
("public", "editor", {"public": ["editor"]}),
|
||||
(
|
||||
"authenticated",
|
||||
"reader",
|
||||
{
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
"authenticated",
|
||||
"editor",
|
||||
{"authenticated": ["editor"], "public": ["editor"]},
|
||||
),
|
||||
(
|
||||
"restricted",
|
||||
"reader",
|
||||
{
|
||||
"restricted": None,
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
"restricted",
|
||||
"editor",
|
||||
{
|
||||
"restricted": None,
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"public": ["editor"],
|
||||
},
|
||||
),
|
||||
([{"link_reach": "public", "link_role": "editor"}], {"public": ["editor"]}),
|
||||
# Edge cases
|
||||
(
|
||||
[{"link_reach": "authenticated", "link_role": "reader"}],
|
||||
"public",
|
||||
None,
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[{"link_reach": "authenticated", "link_role": "editor"}],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[{"link_reach": "restricted", "link_role": "reader"}],
|
||||
{
|
||||
"restricted": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[{"link_reach": "restricted", "link_role": "editor"}],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
# Multiple ancestors with different roles
|
||||
(
|
||||
[
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "editor"},
|
||||
],
|
||||
{"public": ["editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "reader"},
|
||||
{"link_reach": "restricted", "link_role": "editor"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
# Multiple ancestors with different reaches
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "reader"},
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
# Multiple ancestors with mixed reaches and roles
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "editor"},
|
||||
],
|
||||
{"public": ["editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "editor"},
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "reader"},
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
# No ancestors (edge case)
|
||||
(
|
||||
[],
|
||||
None,
|
||||
"reader",
|
||||
{
|
||||
"public": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
),
|
||||
(
|
||||
None,
|
||||
None,
|
||||
{
|
||||
"public": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_models_documents_get_select_options(ancestors_links, select_options):
|
||||
def test_models_documents_get_select_options(reach, role, select_options):
|
||||
"""Validate that the "get_select_options" method operates as expected."""
|
||||
assert models.LinkReachChoices.get_select_options(ancestors_links) == select_options
|
||||
assert models.LinkReachChoices.get_select_options(reach, role) == select_options
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_no_highest_readable():
|
||||
"""Test the compute_ancestors_links method."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
assert document.compute_ancestors_links(user=AnonymousUser()) == []
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_highest_readable(
|
||||
def test_models_documents_compute_ancestors_links_paths_mapping_single(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Test the compute_ancestors_links method."""
|
||||
"""Test the compute_ancestors_links_paths_mapping method on a single document."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
with django_assert_num_queries(1):
|
||||
assert document.compute_ancestors_links_paths_mapping() == {
|
||||
document.path: [{"link_reach": "public", "link_role": document.link_role}]
|
||||
}
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_paths_mapping_structure(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Test the compute_ancestors_links_paths_mapping method on a tree of documents."""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
root = factories.DocumentFactory(
|
||||
link_reach="restricted", link_role="reader", users=[user]
|
||||
)
|
||||
|
||||
factories.DocumentFactory(
|
||||
parent=root, link_reach="public", link_role="reader", users=[user]
|
||||
)
|
||||
child2 = factories.DocumentFactory(
|
||||
root = factories.DocumentFactory(link_reach="restricted", users=[user])
|
||||
document = factories.DocumentFactory(
|
||||
parent=root,
|
||||
link_reach="authenticated",
|
||||
link_role="editor",
|
||||
users=[user, other_user],
|
||||
)
|
||||
child3 = factories.DocumentFactory(
|
||||
parent=child2,
|
||||
sibling = factories.DocumentFactory(parent=root, link_reach="public", users=[user])
|
||||
child = factories.DocumentFactory(
|
||||
parent=document,
|
||||
link_reach="authenticated",
|
||||
link_role="reader",
|
||||
users=[user, other_user],
|
||||
)
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
assert child3.compute_ancestors_links(user=user) == [
|
||||
{"link_reach": root.link_reach, "link_role": root.link_role},
|
||||
{"link_reach": child2.link_reach, "link_role": child2.link_role},
|
||||
]
|
||||
# Child
|
||||
with django_assert_num_queries(1):
|
||||
assert child.compute_ancestors_links_paths_mapping() == {
|
||||
root.path: [{"link_reach": "restricted", "link_role": root.link_role}],
|
||||
document.path: [
|
||||
{"link_reach": "restricted", "link_role": root.link_role},
|
||||
{"link_reach": document.link_reach, "link_role": document.link_role},
|
||||
],
|
||||
child.path: [
|
||||
{"link_reach": "restricted", "link_role": root.link_role},
|
||||
{"link_reach": document.link_reach, "link_role": document.link_role},
|
||||
{"link_reach": child.link_reach, "link_role": child.link_role},
|
||||
],
|
||||
}
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
assert child3.compute_ancestors_links(user=other_user) == [
|
||||
{"link_reach": child2.link_reach, "link_role": child2.link_role},
|
||||
]
|
||||
# Sibling
|
||||
with django_assert_num_queries(1):
|
||||
assert sibling.compute_ancestors_links_paths_mapping() == {
|
||||
root.path: [{"link_reach": "restricted", "link_role": root.link_role}],
|
||||
sibling.path: [
|
||||
{"link_reach": "restricted", "link_role": root.link_role},
|
||||
{"link_reach": sibling.link_reach, "link_role": sibling.link_role},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user