(backend) add document path and depth to accesses endpoint

The frontend requires this information about the ancestor document
to which each access is related. We make sure it does not generate
more db queries and does not fetch useless and heavy fields from
the document like "excerpt".
This commit is contained in:
Samuel Paccoud - DINUM
2025-05-07 18:48:08 +02:00
committed by Anthony LC
parent 433cead0ac
commit 50faf766c8
5 changed files with 118 additions and 92 deletions

View File

@@ -38,83 +38,6 @@ class UserLightSerializer(UserSerializer):
read_only_fields = ["full_name", "short_name"] read_only_fields = ["full_name", "short_name"]
class DocumentAccessSerializer(serializers.ModelSerializer):
"""Serialize document accesses."""
document_id = serializers.PrimaryKeyRelatedField(
read_only=True,
source="document",
)
user_id = serializers.PrimaryKeyRelatedField(
queryset=models.User.objects.all(),
write_only=True,
source="user",
required=False,
allow_null=True,
)
user = UserSerializer(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
max_ancestors_role = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.DocumentAccess
resource_field_name = "document"
fields = [
"id",
"document_id",
"user",
"user_id",
"team",
"role",
"abilities",
"max_ancestors_role",
]
read_only_fields = ["id", "document_id", "abilities", "max_ancestors_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 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 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_id",
"user",
"team",
"role",
"abilities",
"max_ancestors_role",
]
read_only_fields = [
"id",
"document_id",
"team",
"role",
"abilities",
"max_ancestors_role",
]
class TemplateAccessSerializer(serializers.ModelSerializer): class TemplateAccessSerializer(serializers.ModelSerializer):
"""Serialize template accesses.""" """Serialize template accesses."""
@@ -223,6 +146,15 @@ class ListDocumentSerializer(serializers.ModelSerializer):
return instance.get_role(request.user) if request else None 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): class DocumentSerializer(ListDocumentSerializer):
"""Serialize documents with all fields for display in detail views.""" """Serialize documents with all fields for display in detail views."""
@@ -359,6 +291,82 @@ class DocumentSerializer(ListDocumentSerializer):
return super().save(**kwargs) 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)
class Meta:
model = models.DocumentAccess
resource_field_name = "document"
fields = [
"id",
"document",
"user",
"user_id",
"team",
"role",
"abilities",
"max_ancestors_role",
]
read_only_fields = ["id", "document", "abilities", "max_ancestors_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 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",
]
read_only_fields = [
"id",
"document",
"team",
"role",
"abilities",
"max_ancestors_role",
]
class ServerCreateDocumentSerializer(serializers.Serializer): class ServerCreateDocumentSerializer(serializers.Serializer):
""" """
Serializer for creating a document from a server-to-server request. Serializer for creating a document from a server-to-server request.

View File

@@ -1530,11 +1530,7 @@ class DocumentAccessViewSet(
if role not in choices.PRIVILEGED_ROLES: if role not in choices.PRIVILEGED_ROLES:
queryset = queryset.filter(role__in=choices.PRIVILEGED_ROLES) queryset = queryset.filter(role__in=choices.PRIVILEGED_ROLES)
accesses = list( accesses = list(queryset.order_by("document__path"))
queryset.annotate(document_path=db.F("document__path")).order_by(
"document_path"
)
)
# Annotate more information on roles # Annotate more information on roles
path_to_key_to_max_ancestors_role = defaultdict( path_to_key_to_max_ancestors_role = defaultdict(

View File

@@ -1123,9 +1123,7 @@ class DocumentAccess(BaseAccess):
if self.role == RoleChoices.OWNER: if self.role == RoleChoices.OWNER:
can_delete = role == RoleChoices.OWNER and ( can_delete = role == RoleChoices.OWNER and (
# check if document is not root trying to avoid an extra query # check if document is not root trying to avoid an extra query
# "document_path" is annotated by the viewset's list method self.document.depth > 1
len(getattr(self, "document_path", "")) > Document.steplen
or not self.document.is_root()
or DocumentAccess.objects.filter( or DocumentAccess.objects.filter(
document_id=self.document_id, role=RoleChoices.OWNER document_id=self.document_id, role=RoleChoices.OWNER
).count() ).count()

View File

@@ -139,7 +139,11 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
[ [
{ {
"id": str(access.id), "id": str(access.id),
"document_id": str(access.document_id), "document": {
"id": str(access.document_id),
"path": access.document.path,
"depth": access.document.depth,
},
"user": { "user": {
"full_name": access.user.full_name, "full_name": access.user.full_name,
"short_name": access.user.short_name, "short_name": access.user.short_name,
@@ -234,7 +238,11 @@ def test_api_document_accesses_list_authenticated_related_privileged(
[ [
{ {
"id": str(access.id), "id": str(access.id),
"document_id": str(access.document_id), "document": {
"id": str(access.document_id),
"path": access.document.path,
"depth": access.document.depth,
},
"user": { "user": {
"id": str(access.user.id), "id": str(access.user.id),
"email": access.user.email, "email": access.user.email,
@@ -600,7 +608,11 @@ def test_api_document_accesses_retrieve_authenticated_related(
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"id": str(access.id), "id": str(access.id),
"document_id": str(access.document_id), "document": {
"id": str(access.document_id),
"path": access.document.path,
"depth": access.document.depth,
},
"user": access_user, "user": access_user,
"team": "", "team": "",
"role": access.role, "role": access.role,

View File

@@ -170,12 +170,16 @@ def test_api_document_accesses_create_authenticated_administrator(
other_user = serializers.UserSerializer(instance=other_user).data other_user = serializers.UserSerializer(instance=other_user).data
assert response.json() == { assert response.json() == {
"abilities": new_document_access.get_abilities(user), "abilities": new_document_access.get_abilities(user),
"document_id": str(new_document_access.document_id), "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), "id": str(new_document_access.id),
"user": other_user,
"team": "", "team": "",
"role": role, "role": role,
"max_ancestors_role": None, "max_ancestors_role": None,
"user": other_user,
} }
assert len(mail.outbox) == 1 assert len(mail.outbox) == 1
email = mail.outbox[0] email = mail.outbox[0]
@@ -236,7 +240,11 @@ def test_api_document_accesses_create_authenticated_owner(via, depth, mock_user_
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get() new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
other_user = serializers.UserSerializer(instance=other_user).data other_user = serializers.UserSerializer(instance=other_user).data
assert response.json() == { assert response.json() == {
"document_id": str(new_document_access.document_id), "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), "id": str(new_document_access.id),
"user": other_user, "user": other_user,
"team": "", "team": "",
@@ -302,7 +310,11 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
).get() ).get()
other_user_data = serializers.UserSerializer(instance=other_user).data other_user_data = serializers.UserSerializer(instance=other_user).data
assert response.json() == { assert response.json() == {
"document_id": str(new_document_access.document_id), "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), "id": str(new_document_access.id),
"user": other_user_data, "user": other_user_data,
"team": "", "team": "",