2024-03-07 19:46:46 +01:00
|
|
|
"""Client serializers for the impress core app."""
|
2024-08-20 16:42:27 +02:00
|
|
|
|
2024-08-19 22:35:48 +02:00
|
|
|
import mimetypes
|
|
|
|
|
|
|
|
|
|
from django.conf import settings
|
2024-03-03 08:49:27 +01:00
|
|
|
from django.db.models import Q
|
2024-02-09 19:32:12 +01:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
2024-01-09 15:30:36 +01:00
|
|
|
from rest_framework import exceptions, serializers
|
|
|
|
|
|
|
|
|
|
from core import models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize users."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.User
|
2024-09-30 15:13:42 +02:00
|
|
|
fields = ["id", "email", "full_name", "short_name"]
|
|
|
|
|
read_only_fields = ["id", "email", "full_name", "short_name"]
|
2024-01-09 15:30:36 +01:00
|
|
|
|
2024-02-09 19:32:12 +01:00
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class BaseAccessSerializer(serializers.ModelSerializer):
|
2024-02-09 19:32:12 +01:00
|
|
|
"""Serialize template accesses."""
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
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
|
2024-02-09 19:32:12 +01:00
|
|
|
else "You are not allowed to set this role for this template."
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
|
|
|
|
raise exceptions.PermissionDenied(message)
|
|
|
|
|
|
|
|
|
|
# Create
|
|
|
|
|
else:
|
|
|
|
|
try:
|
2024-04-03 18:50:28 +02:00
|
|
|
resource_id = self.context["resource_id"]
|
2024-01-09 15:30:36 +01:00
|
|
|
except KeyError as exc:
|
|
|
|
|
raise exceptions.ValidationError(
|
2024-04-03 18:50:28 +02:00
|
|
|
"You must set a resource ID in kwargs to create a new access."
|
2024-01-09 15:30:36 +01:00
|
|
|
) from exc
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
if not self.Meta.model.objects.filter( # pylint: disable=no-member
|
2024-09-06 16:12:02 +02:00
|
|
|
Q(user=user) | Q(team__in=user.teams),
|
2024-01-09 15:30:36 +01:00
|
|
|
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
2024-10-11 20:42:16 +02:00
|
|
|
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
2024-01-09 15:30:36 +01:00
|
|
|
).exists():
|
|
|
|
|
raise exceptions.PermissionDenied(
|
2024-04-03 18:50:28 +02:00
|
|
|
"You are not allowed to manage accesses for this resource."
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
role == models.RoleChoices.OWNER
|
2024-04-03 18:50:28 +02:00
|
|
|
and not self.Meta.model.objects.filter( # pylint: disable=no-member
|
2024-09-06 16:12:02 +02:00
|
|
|
Q(user=user) | Q(team__in=user.teams),
|
2024-01-09 15:30:36 +01:00
|
|
|
role=models.RoleChoices.OWNER,
|
2024-04-03 18:50:28 +02:00
|
|
|
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
2024-01-09 15:30:36 +01:00
|
|
|
).exists()
|
|
|
|
|
):
|
|
|
|
|
raise exceptions.PermissionDenied(
|
2024-04-03 18:50:28 +02:00
|
|
|
"Only owners of a resource can assign other users as owners."
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
# pylint: disable=no-member
|
|
|
|
|
attrs[f"{self.Meta.resource_field_name}_id"] = self.context["resource_id"]
|
2024-01-09 15:30:36 +01:00
|
|
|
return attrs
|
|
|
|
|
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class DocumentAccessSerializer(BaseAccessSerializer):
|
|
|
|
|
"""Serialize document accesses."""
|
|
|
|
|
|
2024-05-31 17:06:39 +02:00
|
|
|
user_id = serializers.PrimaryKeyRelatedField(
|
|
|
|
|
queryset=models.User.objects.all(),
|
|
|
|
|
write_only=True,
|
|
|
|
|
source="user",
|
|
|
|
|
required=False,
|
|
|
|
|
allow_null=True,
|
|
|
|
|
)
|
|
|
|
|
user = UserSerializer(read_only=True)
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class Meta:
|
|
|
|
|
model = models.DocumentAccess
|
|
|
|
|
resource_field_name = "document"
|
2024-05-31 17:06:39 +02:00
|
|
|
fields = ["id", "user", "user_id", "team", "role", "abilities"]
|
2024-04-03 18:50:28 +02:00
|
|
|
read_only_fields = ["id", "abilities"]
|
2024-01-09 15:30:36 +01:00
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
|
|
|
|
|
class TemplateAccessSerializer(BaseAccessSerializer):
|
|
|
|
|
"""Serialize template accesses."""
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
class Meta:
|
2024-04-03 18:50:28 +02:00
|
|
|
model = models.TemplateAccess
|
|
|
|
|
resource_field_name = "template"
|
|
|
|
|
fields = ["id", "user", "team", "role", "abilities"]
|
|
|
|
|
read_only_fields = ["id", "abilities"]
|
2024-01-09 15:30:36 +01:00
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
|
|
|
|
|
class BaseResourceSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize documents."""
|
|
|
|
|
|
|
|
|
|
abilities = serializers.SerializerMethodField(read_only=True)
|
|
|
|
|
accesses = TemplateAccessSerializer(many=True, read_only=True)
|
|
|
|
|
|
|
|
|
|
def get_abilities(self, document) -> dict:
|
2024-01-09 15:30:36 +01:00
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request:
|
2024-04-03 18:50:28 +02:00
|
|
|
return document.get_abilities(request.user)
|
2024-01-09 15:30:36 +01:00
|
|
|
return {}
|
2024-02-09 19:32:12 +01:00
|
|
|
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class DocumentSerializer(BaseResourceSerializer):
|
|
|
|
|
"""Serialize documents."""
|
|
|
|
|
|
2024-05-21 14:46:23 +02:00
|
|
|
content = serializers.CharField(required=False)
|
2024-05-31 17:06:39 +02:00
|
|
|
accesses = DocumentAccessSerializer(many=True, read_only=True)
|
2024-04-06 09:09:46 +02:00
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class Meta:
|
|
|
|
|
model = models.Document
|
2024-07-02 12:47:22 +02:00
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"content",
|
|
|
|
|
"title",
|
|
|
|
|
"accesses",
|
|
|
|
|
"abilities",
|
2024-09-08 23:37:49 +02:00
|
|
|
"link_role",
|
|
|
|
|
"link_reach",
|
|
|
|
|
"created_at",
|
|
|
|
|
"updated_at",
|
|
|
|
|
]
|
|
|
|
|
read_only_fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"accesses",
|
|
|
|
|
"abilities",
|
|
|
|
|
"link_role",
|
|
|
|
|
"link_reach",
|
2024-07-02 12:47:22 +02:00
|
|
|
"created_at",
|
|
|
|
|
"updated_at",
|
|
|
|
|
]
|
2024-04-03 18:50:28 +02:00
|
|
|
|
2024-09-09 19:31:26 +02:00
|
|
|
def get_fields(self):
|
|
|
|
|
"""Dynamically make `id` read-only on PUT requests but writable on POST requests."""
|
|
|
|
|
fields = super().get_fields()
|
|
|
|
|
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request and request.method == "POST":
|
|
|
|
|
fields["id"].read_only = False
|
|
|
|
|
|
|
|
|
|
return fields
|
|
|
|
|
|
|
|
|
|
def validate_id(self, value):
|
|
|
|
|
"""Ensure the provided ID does not already exist when creating a new document."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
|
|
|
|
|
# Only check this on POST (creation)
|
|
|
|
|
if request and request.method == "POST":
|
|
|
|
|
if models.Document.objects.filter(id=value).exists():
|
|
|
|
|
raise serializers.ValidationError(
|
|
|
|
|
"A document with this ID already exists. You cannot override it."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
|
2024-09-08 23:07:47 +02:00
|
|
|
class LinkDocumentSerializer(BaseResourceSerializer):
|
|
|
|
|
"""
|
|
|
|
|
Serialize link configuration for documents.
|
|
|
|
|
We expose it separately from document in order to simplify and secure access control.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Document
|
|
|
|
|
fields = [
|
|
|
|
|
"link_role",
|
|
|
|
|
"link_reach",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2024-08-19 22:35:48 +02:00
|
|
|
# Suppress the warning about not implementing `create` and `update` methods
|
|
|
|
|
# since we don't use a model and only rely on the serializer for validation
|
|
|
|
|
# pylint: disable=abstract-method
|
|
|
|
|
class FileUploadSerializer(serializers.Serializer):
|
|
|
|
|
"""Receive file upload requests."""
|
|
|
|
|
|
|
|
|
|
file = serializers.FileField()
|
|
|
|
|
|
|
|
|
|
def validate_file(self, file):
|
|
|
|
|
"""Add file size and type constraints as defined in settings."""
|
|
|
|
|
# Validate file size
|
|
|
|
|
if file.size > settings.DOCUMENT_IMAGE_MAX_SIZE:
|
|
|
|
|
max_size = settings.DOCUMENT_IMAGE_MAX_SIZE // (1024 * 1024)
|
|
|
|
|
raise serializers.ValidationError(
|
|
|
|
|
f"File size exceeds the maximum limit of {max_size:d} MB."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Validate file type
|
|
|
|
|
mime_type, _ = mimetypes.guess_type(file.name)
|
|
|
|
|
if mime_type not in settings.DOCUMENT_IMAGE_ALLOWED_MIME_TYPES:
|
|
|
|
|
mime_types = ", ".join(settings.DOCUMENT_IMAGE_ALLOWED_MIME_TYPES)
|
|
|
|
|
raise serializers.ValidationError(
|
|
|
|
|
f"File type '{mime_type:s}' is not allowed. Allowed types are: {mime_types:s}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return file
|
|
|
|
|
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class TemplateSerializer(BaseResourceSerializer):
|
|
|
|
|
"""Serialize templates."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Template
|
2024-04-17 17:11:31 +02:00
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"title",
|
|
|
|
|
"accesses",
|
|
|
|
|
"abilities",
|
|
|
|
|
"css",
|
|
|
|
|
"code",
|
|
|
|
|
"is_public",
|
|
|
|
|
]
|
2024-04-03 18:50:28 +02:00
|
|
|
read_only_fields = ["id", "accesses", "abilities"]
|
|
|
|
|
|
|
|
|
|
|
2024-02-09 19:32:12 +01:00
|
|
|
# pylint: disable=abstract-method
|
|
|
|
|
class DocumentGenerationSerializer(serializers.Serializer):
|
|
|
|
|
"""Serializer to receive a request to generate a document on a template."""
|
|
|
|
|
|
2024-04-16 10:30:10 +02:00
|
|
|
body = serializers.CharField(label=_("Body"))
|
|
|
|
|
body_type = serializers.ChoiceField(
|
|
|
|
|
choices=["html", "markdown"],
|
|
|
|
|
label=_("Body type"),
|
|
|
|
|
required=False,
|
|
|
|
|
default="html",
|
|
|
|
|
)
|
2024-08-07 14:44:18 +02:00
|
|
|
format = serializers.ChoiceField(
|
|
|
|
|
choices=["pdf", "docx"],
|
|
|
|
|
label=_("Format"),
|
|
|
|
|
required=False,
|
|
|
|
|
default="pdf",
|
|
|
|
|
)
|
2024-05-13 23:31:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvitationSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize invitations."""
|
|
|
|
|
|
|
|
|
|
abilities = serializers.SerializerMethodField(read_only=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Invitation
|
|
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"abilities",
|
|
|
|
|
"created_at",
|
|
|
|
|
"email",
|
|
|
|
|
"document",
|
|
|
|
|
"role",
|
|
|
|
|
"issuer",
|
|
|
|
|
"is_expired",
|
|
|
|
|
]
|
|
|
|
|
read_only_fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"abilities",
|
|
|
|
|
"created_at",
|
|
|
|
|
"document",
|
|
|
|
|
"issuer",
|
|
|
|
|
"is_expired",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def get_abilities(self, invitation) -> dict:
|
|
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request:
|
|
|
|
|
return invitation.get_abilities(request.user)
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
def validate(self, attrs):
|
|
|
|
|
"""Validate and restrict invitation to new user based on email."""
|
|
|
|
|
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
user = getattr(request, "user", None)
|
|
|
|
|
role = attrs.get("role")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
document_id = self.context["resource_id"]
|
|
|
|
|
except KeyError as exc:
|
|
|
|
|
raise exceptions.ValidationError(
|
|
|
|
|
"You must set a document ID in kwargs to create a new document invitation."
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
if not user and user.is_authenticated:
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"Anonymous users are not allowed to create invitations."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not models.DocumentAccess.objects.filter(
|
2024-09-06 16:12:02 +02:00
|
|
|
Q(user=user) | Q(team__in=user.teams),
|
2024-05-13 23:31:00 +02:00
|
|
|
document=document_id,
|
|
|
|
|
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
|
|
|
|
).exists():
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"You are not allowed to manage invitations for this document."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
role == models.RoleChoices.OWNER
|
|
|
|
|
and not models.DocumentAccess.objects.filter(
|
2024-09-06 16:12:02 +02:00
|
|
|
Q(user=user) | Q(team__in=user.teams),
|
2024-05-13 23:31:00 +02:00
|
|
|
document=document_id,
|
|
|
|
|
role=models.RoleChoices.OWNER,
|
|
|
|
|
).exists()
|
|
|
|
|
):
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"Only owners of a document can invite other users as owners."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
attrs["document_id"] = document_id
|
|
|
|
|
attrs["issuer"] = user
|
|
|
|
|
return attrs
|
2024-07-17 09:10:05 +02:00
|
|
|
|
|
|
|
|
|
2024-09-16 19:27:48 +02:00
|
|
|
class VersionFilterSerializer(serializers.Serializer):
|
|
|
|
|
"""Validate version filters applied to the list endpoint."""
|
2024-07-17 09:10:05 +02:00
|
|
|
|
2024-09-16 19:27:48 +02:00
|
|
|
version_id = serializers.CharField(required=False, allow_blank=True)
|
|
|
|
|
page_size = serializers.IntegerField(
|
|
|
|
|
required=False, min_value=1, max_value=50, default=20
|
|
|
|
|
)
|