2024-01-09 15:30:36 +01:00
|
|
|
"""API endpoints"""
|
2024-08-15 15:38:38 +02:00
|
|
|
from core.utils import email_invitation
|
2024-03-03 08:49:27 +01:00
|
|
|
from django.contrib.postgres.aggregates import ArrayAgg
|
2024-01-09 15:30:36 +01:00
|
|
|
from django.db.models import (
|
|
|
|
|
OuterRef,
|
|
|
|
|
Q,
|
|
|
|
|
Subquery,
|
|
|
|
|
)
|
2024-08-07 14:44:18 +02:00
|
|
|
from django.http import Http404
|
2024-01-09 15:30:36 +01:00
|
|
|
|
2024-04-08 23:37:15 +02:00
|
|
|
from botocore.exceptions import ClientError
|
2024-01-09 15:30:36 +01:00
|
|
|
from rest_framework import (
|
|
|
|
|
decorators,
|
|
|
|
|
exceptions,
|
2024-04-16 09:56:18 +02:00
|
|
|
filters,
|
2024-01-09 15:30:36 +01:00
|
|
|
mixins,
|
|
|
|
|
pagination,
|
2024-02-09 19:32:12 +01:00
|
|
|
status,
|
2024-01-09 15:30:36 +01:00
|
|
|
viewsets,
|
|
|
|
|
)
|
2024-02-09 19:32:12 +01:00
|
|
|
from rest_framework import (
|
|
|
|
|
response as drf_response,
|
|
|
|
|
)
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
from core import models
|
|
|
|
|
|
|
|
|
|
from . import permissions, serializers
|
|
|
|
|
|
2024-02-09 19:32:12 +01:00
|
|
|
# pylint: disable=too-many-ancestors
|
|
|
|
|
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
class NestedGenericViewSet(viewsets.GenericViewSet):
|
|
|
|
|
"""
|
|
|
|
|
A generic Viewset aims to be used in a nested route context.
|
|
|
|
|
e.g: `/api/v1.0/resource_1/<resource_1_pk>/resource_2/<resource_2_pk>/`
|
|
|
|
|
|
|
|
|
|
It allows to define all url kwargs and lookup fields to perform the lookup.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
lookup_fields: list[str] = ["pk"]
|
|
|
|
|
lookup_url_kwargs: list[str] = []
|
|
|
|
|
|
|
|
|
|
def __getattribute__(self, item):
|
|
|
|
|
"""
|
|
|
|
|
This method is overridden to allow to get the last lookup field or lookup url kwarg
|
|
|
|
|
when accessing the `lookup_field` or `lookup_url_kwarg` attribute. This is useful
|
|
|
|
|
to keep compatibility with all methods used by the parent class `GenericViewSet`.
|
|
|
|
|
"""
|
|
|
|
|
if item in ["lookup_field", "lookup_url_kwarg"]:
|
|
|
|
|
return getattr(self, item + "s", [None])[-1]
|
|
|
|
|
|
|
|
|
|
return super().__getattribute__(item)
|
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
"""
|
|
|
|
|
Get the list of items for this view.
|
|
|
|
|
|
|
|
|
|
`lookup_fields` attribute is enumerated here to perform the nested lookup.
|
|
|
|
|
"""
|
|
|
|
|
queryset = super().get_queryset()
|
|
|
|
|
|
|
|
|
|
# The last lookup field is removed to perform the nested lookup as it corresponds
|
|
|
|
|
# to the object pk, it is used within get_object method.
|
|
|
|
|
lookup_url_kwargs = (
|
|
|
|
|
self.lookup_url_kwargs[:-1]
|
|
|
|
|
if self.lookup_url_kwargs
|
|
|
|
|
else self.lookup_fields[:-1]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
filter_kwargs = {}
|
|
|
|
|
for index, lookup_url_kwarg in enumerate(lookup_url_kwargs):
|
|
|
|
|
if lookup_url_kwarg not in self.kwargs:
|
|
|
|
|
raise KeyError(
|
|
|
|
|
f"Expected view {self.__class__.__name__} to be called with a URL "
|
|
|
|
|
f'keyword argument named "{lookup_url_kwarg}". Fix your URL conf, or '
|
|
|
|
|
"set the `.lookup_fields` attribute on the view correctly."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
filter_kwargs.update(
|
|
|
|
|
{self.lookup_fields[index]: self.kwargs[lookup_url_kwarg]}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return queryset.filter(**filter_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SerializerPerActionMixin:
|
|
|
|
|
"""
|
|
|
|
|
A mixin to allow to define serializer classes for each action.
|
|
|
|
|
|
|
|
|
|
This mixin is useful to avoid to define a serializer class for each action in the
|
|
|
|
|
`get_serializer_class` method.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
serializer_classes: dict[str, type] = {}
|
|
|
|
|
default_serializer_class: type = None
|
|
|
|
|
|
|
|
|
|
def get_serializer_class(self):
|
|
|
|
|
"""
|
|
|
|
|
Return the serializer class to use depending on the action.
|
|
|
|
|
"""
|
|
|
|
|
return self.serializer_classes.get(self.action, self.default_serializer_class)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Pagination(pagination.PageNumberPagination):
|
|
|
|
|
"""Pagination to display no more than 100 objects per page sorted by creation date."""
|
|
|
|
|
|
|
|
|
|
ordering = "-created_on"
|
|
|
|
|
max_page_size = 100
|
|
|
|
|
page_size_query_param = "page_size"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserViewSet(
|
2024-05-29 14:48:12 +02:00
|
|
|
mixins.UpdateModelMixin, viewsets.GenericViewSet, mixins.ListModelMixin
|
2024-01-09 15:30:36 +01:00
|
|
|
):
|
|
|
|
|
"""User ViewSet"""
|
|
|
|
|
|
|
|
|
|
permission_classes = [permissions.IsSelf]
|
|
|
|
|
queryset = models.User.objects.all()
|
|
|
|
|
serializer_class = serializers.UserSerializer
|
|
|
|
|
|
2024-05-29 14:48:12 +02:00
|
|
|
def get_queryset(self):
|
|
|
|
|
"""
|
|
|
|
|
Limit listed users by querying the email field with a trigram similarity
|
|
|
|
|
search if a query is provided.
|
|
|
|
|
Limit listed users by excluding users already in the document if a document_id
|
|
|
|
|
is provided.
|
|
|
|
|
"""
|
|
|
|
|
queryset = self.queryset
|
|
|
|
|
|
|
|
|
|
if self.action == "list":
|
|
|
|
|
# Exclude all users already in the given document
|
|
|
|
|
if document_id := self.request.GET.get("document_id", ""):
|
|
|
|
|
queryset = queryset.exclude(documentaccess__document_id=document_id)
|
|
|
|
|
|
|
|
|
|
# Filter users by email similarity
|
|
|
|
|
if query := self.request.GET.get("q", ""):
|
|
|
|
|
queryset = queryset.filter(email__trigram_word_similar=query)
|
|
|
|
|
|
|
|
|
|
return queryset
|
|
|
|
|
|
2024-01-09 15:30:36 +01:00
|
|
|
@decorators.action(
|
|
|
|
|
detail=False,
|
|
|
|
|
methods=["get"],
|
|
|
|
|
url_name="me",
|
|
|
|
|
url_path="me",
|
|
|
|
|
permission_classes=[permissions.IsAuthenticated],
|
|
|
|
|
)
|
|
|
|
|
def get_me(self, request):
|
|
|
|
|
"""
|
|
|
|
|
Return information on currently logged user
|
|
|
|
|
"""
|
|
|
|
|
context = {"request": request}
|
2024-02-09 19:32:12 +01:00
|
|
|
return drf_response.Response(
|
2024-01-09 15:30:36 +01:00
|
|
|
self.serializer_class(request.user, context=context).data
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class ResourceViewsetMixin:
|
|
|
|
|
"""Mixin with methods common to all resource viewsets that are managed with accesses."""
|
2024-01-09 15:30:36 +01:00
|
|
|
|
2024-04-16 09:56:18 +02:00
|
|
|
filter_backends = [filters.OrderingFilter]
|
2024-07-04 12:02:31 +02:00
|
|
|
ordering_fields = ["created_at", "updated_at", "title"]
|
2024-04-16 09:56:18 +02:00
|
|
|
ordering = ["-created_at"]
|
|
|
|
|
|
2024-01-09 15:30:36 +01:00
|
|
|
def get_queryset(self):
|
2024-04-03 18:50:28 +02:00
|
|
|
"""Custom queryset to get user related resources."""
|
|
|
|
|
queryset = super().get_queryset()
|
2024-02-09 19:32:12 +01:00
|
|
|
if not self.request.user.is_authenticated:
|
2024-04-03 18:50:28 +02:00
|
|
|
return queryset.filter(is_public=True)
|
2024-02-09 19:32:12 +01:00
|
|
|
|
2024-03-03 08:49:27 +01:00
|
|
|
user = self.request.user
|
|
|
|
|
teams = user.get_teams()
|
|
|
|
|
|
|
|
|
|
user_roles_query = (
|
2024-04-03 18:50:28 +02:00
|
|
|
self.access_model_class.objects.filter(
|
|
|
|
|
Q(user=user) | Q(team__in=teams),
|
|
|
|
|
**{self.resource_field_name: OuterRef("pk")},
|
2024-03-03 08:49:27 +01:00
|
|
|
)
|
2024-04-03 18:50:28 +02:00
|
|
|
.values(self.resource_field_name)
|
2024-03-03 08:49:27 +01:00
|
|
|
.annotate(roles_array=ArrayAgg("role"))
|
|
|
|
|
.values("roles_array")
|
|
|
|
|
)
|
2024-02-09 19:32:12 +01:00
|
|
|
return (
|
2024-04-03 18:50:28 +02:00
|
|
|
queryset.filter(
|
2024-03-03 08:49:27 +01:00
|
|
|
Q(accesses__user=user) | Q(accesses__team__in=teams) | Q(is_public=True)
|
2024-02-09 19:32:12 +01:00
|
|
|
)
|
2024-03-03 08:49:27 +01:00
|
|
|
.annotate(user_roles=Subquery(user_roles_query))
|
2024-02-09 19:32:12 +01:00
|
|
|
.distinct()
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def perform_create(self, serializer):
|
2024-04-03 18:50:28 +02:00
|
|
|
"""Set the current user as owner of the newly created object."""
|
|
|
|
|
obj = serializer.save()
|
|
|
|
|
self.access_model_class.objects.create(
|
2024-01-09 15:30:36 +01:00
|
|
|
user=self.request.user,
|
|
|
|
|
role=models.RoleChoices.OWNER,
|
2024-04-03 18:50:28 +02:00
|
|
|
**{self.resource_field_name: obj},
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
class ResourceAccessViewsetMixin:
|
|
|
|
|
"""Mixin with methods common to all access viewsets."""
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
def get_permissions(self):
|
2024-04-03 18:50:28 +02:00
|
|
|
"""User only needs to be authenticated to list resource accesses"""
|
2024-01-09 15:30:36 +01:00
|
|
|
if self.action == "list":
|
|
|
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
|
else:
|
|
|
|
|
return super().get_permissions()
|
|
|
|
|
|
|
|
|
|
return [permission() for permission in permission_classes]
|
|
|
|
|
|
|
|
|
|
def get_serializer_context(self):
|
|
|
|
|
"""Extra context provided to the serializer class."""
|
|
|
|
|
context = super().get_serializer_context()
|
2024-04-03 18:50:28 +02:00
|
|
|
context["resource_id"] = self.kwargs["resource_id"]
|
2024-01-09 15:30:36 +01:00
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
"""Return the queryset according to the action."""
|
|
|
|
|
queryset = super().get_queryset()
|
2024-04-03 18:50:28 +02:00
|
|
|
queryset = queryset.filter(
|
|
|
|
|
**{self.resource_field_name: self.kwargs["resource_id"]}
|
|
|
|
|
)
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
if self.action == "list":
|
2024-03-03 08:49:27 +01:00
|
|
|
user = self.request.user
|
|
|
|
|
teams = user.get_teams()
|
|
|
|
|
|
|
|
|
|
user_roles_query = (
|
2024-04-03 18:50:28 +02:00
|
|
|
queryset.filter(
|
2024-03-03 08:49:27 +01:00
|
|
|
Q(user=user) | Q(team__in=teams),
|
2024-04-03 18:50:28 +02:00
|
|
|
**{self.resource_field_name: self.kwargs["resource_id"]},
|
2024-03-03 08:49:27 +01:00
|
|
|
)
|
2024-04-03 18:50:28 +02:00
|
|
|
.values(self.resource_field_name)
|
2024-03-03 08:49:27 +01:00
|
|
|
.annotate(roles_array=ArrayAgg("role"))
|
|
|
|
|
.values("roles_array")
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
# 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
|
2024-02-09 19:32:12 +01:00
|
|
|
# access instances pointing to the logged-in user)
|
2024-01-09 15:30:36 +01:00
|
|
|
queryset = (
|
|
|
|
|
queryset.filter(
|
2024-04-03 18:50:28 +02:00
|
|
|
Q(**{f"{self.resource_field_name}__accesses__user": user})
|
|
|
|
|
| Q(**{f"{self.resource_field_name}__accesses__team__in": teams}),
|
|
|
|
|
**{self.resource_field_name: self.kwargs["resource_id"]},
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
2024-03-03 08:49:27 +01:00
|
|
|
.annotate(user_roles=Subquery(user_roles_query))
|
2024-01-09 15:30:36 +01:00
|
|
|
.distinct()
|
|
|
|
|
)
|
|
|
|
|
return queryset
|
|
|
|
|
|
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
|
|
|
"""Forbid deleting the last owner access"""
|
|
|
|
|
instance = self.get_object()
|
2024-04-03 18:50:28 +02:00
|
|
|
resource = getattr(instance, self.resource_field_name)
|
2024-01-09 15:30:36 +01:00
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
# Check if the access being deleted is the last owner access for the resource
|
2024-02-09 19:32:12 +01:00
|
|
|
if (
|
|
|
|
|
instance.role == "owner"
|
2024-04-03 18:50:28 +02:00
|
|
|
and resource.accesses.filter(role="owner").count() == 1
|
2024-02-09 19:32:12 +01:00
|
|
|
):
|
|
|
|
|
return drf_response.Response(
|
2024-04-03 18:50:28 +02:00
|
|
|
{"detail": "Cannot delete the last owner access for the resource."},
|
2024-02-09 19:32:12 +01:00
|
|
|
status=403,
|
2024-01-09 15:30:36 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
):
|
2024-04-03 18:50:28 +02:00
|
|
|
resource = getattr(instance, self.resource_field_name)
|
|
|
|
|
# Check if the access being updated is the last owner access for the resource
|
2024-01-09 15:30:36 +01:00
|
|
|
if (
|
|
|
|
|
instance.role == models.RoleChoices.OWNER
|
2024-04-03 18:50:28 +02:00
|
|
|
and resource.accesses.filter(role=models.RoleChoices.OWNER).count() == 1
|
2024-01-09 15:30:36 +01:00
|
|
|
):
|
|
|
|
|
message = "Cannot change the role to a non-owner role for the last owner access."
|
2024-02-09 19:32:12 +01:00
|
|
|
raise exceptions.PermissionDenied({"detail": message})
|
2024-01-09 15:30:36 +01:00
|
|
|
|
|
|
|
|
serializer.save()
|
2024-04-03 18:50:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class DocumentViewSet(
|
|
|
|
|
ResourceViewsetMixin,
|
|
|
|
|
mixins.CreateModelMixin,
|
|
|
|
|
mixins.DestroyModelMixin,
|
|
|
|
|
mixins.ListModelMixin,
|
|
|
|
|
mixins.RetrieveModelMixin,
|
|
|
|
|
mixins.UpdateModelMixin,
|
|
|
|
|
viewsets.GenericViewSet,
|
|
|
|
|
):
|
|
|
|
|
"""Document ViewSet"""
|
|
|
|
|
|
|
|
|
|
permission_classes = [
|
|
|
|
|
permissions.IsAuthenticatedOrSafe,
|
|
|
|
|
permissions.AccessPermission,
|
|
|
|
|
]
|
|
|
|
|
serializer_class = serializers.DocumentSerializer
|
|
|
|
|
access_model_class = models.DocumentAccess
|
|
|
|
|
resource_field_name = "document"
|
|
|
|
|
queryset = models.Document.objects.all()
|
|
|
|
|
|
2024-06-20 15:24:10 +02:00
|
|
|
def perform_create(self, serializer):
|
|
|
|
|
"""
|
|
|
|
|
Override perform_create to use the provided ID in the payload if it exists
|
|
|
|
|
"""
|
|
|
|
|
document_id = self.request.data.get("id")
|
|
|
|
|
document = serializer.save(id=document_id) if document_id else serializer.save()
|
|
|
|
|
|
|
|
|
|
self.access_model_class.objects.create(
|
|
|
|
|
user=self.request.user,
|
|
|
|
|
role=models.RoleChoices.OWNER,
|
|
|
|
|
**{self.resource_field_name: document},
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-08 23:37:15 +02:00
|
|
|
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
|
|
|
|
def versions_list(self, request, *args, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Return the document's versions but only those created after the user got access
|
|
|
|
|
to the document
|
|
|
|
|
"""
|
|
|
|
|
document = self.get_object()
|
|
|
|
|
from_datetime = min(
|
|
|
|
|
access.created_at
|
|
|
|
|
for access in document.accesses.filter(
|
|
|
|
|
Q(user=request.user) | Q(team__in=request.user.get_teams()),
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-07-17 09:10:05 +02:00
|
|
|
|
|
|
|
|
versions_data = document.get_versions_slice(from_datetime=from_datetime)[
|
|
|
|
|
"versions"
|
|
|
|
|
]
|
|
|
|
|
paginator = pagination.PageNumberPagination()
|
|
|
|
|
paginated_versions = paginator.paginate_queryset(versions_data, request)
|
|
|
|
|
serialized_versions = serializers.DocumentVersionSerializer(
|
|
|
|
|
paginated_versions, many=True
|
2024-04-08 23:37:15 +02:00
|
|
|
)
|
|
|
|
|
|
2024-07-17 09:10:05 +02:00
|
|
|
return paginator.get_paginated_response(serialized_versions.data)
|
|
|
|
|
|
2024-04-08 23:37:15 +02:00
|
|
|
@decorators.action(
|
|
|
|
|
detail=True,
|
|
|
|
|
methods=["get", "delete"],
|
|
|
|
|
url_path="versions/(?P<version_id>[0-9a-f-]{36})",
|
|
|
|
|
)
|
|
|
|
|
# pylint: disable=unused-argument
|
|
|
|
|
def versions_detail(self, request, pk, version_id, *args, **kwargs):
|
|
|
|
|
"""Custom action to retrieve a specific version of a document"""
|
|
|
|
|
document = self.get_object()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
response = document.get_content_response(version_id=version_id)
|
|
|
|
|
except (FileNotFoundError, ClientError) as err:
|
|
|
|
|
raise Http404 from err
|
|
|
|
|
|
|
|
|
|
# Don't let users access versions that were created before they were given access
|
|
|
|
|
# to the document
|
|
|
|
|
from_datetime = min(
|
|
|
|
|
access.created_at
|
|
|
|
|
for access in document.accesses.filter(
|
|
|
|
|
Q(user=request.user) | Q(team__in=request.user.get_teams()),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if response["LastModified"] < from_datetime:
|
|
|
|
|
raise Http404
|
|
|
|
|
|
|
|
|
|
if request.method == "DELETE":
|
|
|
|
|
response = document.delete_version(version_id)
|
|
|
|
|
return drf_response.Response(
|
|
|
|
|
status=response["ResponseMetadata"]["HTTPStatusCode"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return drf_response.Response(
|
|
|
|
|
{
|
2024-05-21 14:46:23 +02:00
|
|
|
"content": response["Body"].read().decode("utf-8"),
|
2024-04-08 23:37:15 +02:00
|
|
|
"last_modified": response["LastModified"],
|
2024-07-17 09:10:05 +02:00
|
|
|
"id": version_id,
|
2024-04-08 23:37:15 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-03 18:50:28 +02:00
|
|
|
|
|
|
|
|
class DocumentAccessViewSet(
|
|
|
|
|
ResourceAccessViewsetMixin,
|
|
|
|
|
mixins.CreateModelMixin,
|
|
|
|
|
mixins.DestroyModelMixin,
|
|
|
|
|
mixins.ListModelMixin,
|
|
|
|
|
mixins.RetrieveModelMixin,
|
|
|
|
|
mixins.UpdateModelMixin,
|
|
|
|
|
viewsets.GenericViewSet,
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
API ViewSet for all interactions with document accesses.
|
|
|
|
|
|
|
|
|
|
GET /api/v1.0/documents/<resource_id>/accesses/:<document_access_id>
|
|
|
|
|
Return list of all document accesses related to the logged-in user or one
|
|
|
|
|
document access if an id is provided.
|
|
|
|
|
|
|
|
|
|
POST /api/v1.0/documents/<resource_id>/accesses/ with expected data:
|
|
|
|
|
- user: str
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [administrator|editor|reader]
|
2024-04-03 18:50:28 +02:00
|
|
|
Return newly created document access
|
|
|
|
|
|
|
|
|
|
PUT /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/ with expected data:
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [owner|admin|editor|reader]
|
2024-04-03 18:50:28 +02:00
|
|
|
Return updated document access
|
|
|
|
|
|
|
|
|
|
PATCH /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/ with expected data:
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [owner|admin|editor|reader]
|
2024-04-03 18:50:28 +02:00
|
|
|
Return partially updated document access
|
|
|
|
|
|
|
|
|
|
DELETE /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/
|
|
|
|
|
Delete targeted document access
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
lookup_field = "pk"
|
|
|
|
|
pagination_class = Pagination
|
|
|
|
|
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
|
|
|
|
queryset = models.DocumentAccess.objects.select_related("user").all()
|
|
|
|
|
resource_field_name = "document"
|
|
|
|
|
serializer_class = serializers.DocumentAccessSerializer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TemplateViewSet(
|
|
|
|
|
ResourceViewsetMixin,
|
|
|
|
|
mixins.CreateModelMixin,
|
|
|
|
|
mixins.DestroyModelMixin,
|
|
|
|
|
mixins.ListModelMixin,
|
|
|
|
|
mixins.RetrieveModelMixin,
|
|
|
|
|
mixins.UpdateModelMixin,
|
|
|
|
|
viewsets.GenericViewSet,
|
|
|
|
|
):
|
|
|
|
|
"""Template ViewSet"""
|
|
|
|
|
|
|
|
|
|
permission_classes = [
|
|
|
|
|
permissions.IsAuthenticatedOrSafe,
|
|
|
|
|
permissions.AccessPermission,
|
|
|
|
|
]
|
|
|
|
|
serializer_class = serializers.TemplateSerializer
|
|
|
|
|
access_model_class = models.TemplateAccess
|
|
|
|
|
resource_field_name = "template"
|
|
|
|
|
queryset = models.Template.objects.all()
|
|
|
|
|
|
|
|
|
|
@decorators.action(
|
|
|
|
|
detail=True,
|
|
|
|
|
methods=["post"],
|
|
|
|
|
url_path="generate-document",
|
|
|
|
|
permission_classes=[permissions.AccessPermission],
|
|
|
|
|
)
|
|
|
|
|
# pylint: disable=unused-argument
|
|
|
|
|
def generate_document(self, request, pk=None):
|
|
|
|
|
"""
|
2024-08-07 14:44:18 +02:00
|
|
|
Generate and return a document for this template around the
|
|
|
|
|
body passed as argument.
|
|
|
|
|
|
|
|
|
|
2 types of body are accepted:
|
|
|
|
|
- HTML: body_type = "html"
|
|
|
|
|
- Markdown: body_type = "markdown"
|
|
|
|
|
|
|
|
|
|
2 types of documents can be generated:
|
|
|
|
|
- PDF: format = "pdf"
|
|
|
|
|
- Docx: format = "docx"
|
2024-04-03 18:50:28 +02:00
|
|
|
"""
|
|
|
|
|
serializer = serializers.DocumentGenerationSerializer(data=request.data)
|
|
|
|
|
|
|
|
|
|
if not serializer.is_valid():
|
|
|
|
|
return drf_response.Response(
|
|
|
|
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
body = serializer.validated_data["body"]
|
2024-04-16 10:30:10 +02:00
|
|
|
body_type = serializer.validated_data["body_type"]
|
2024-08-07 14:44:18 +02:00
|
|
|
export_format = serializer.validated_data["format"]
|
2024-04-03 18:50:28 +02:00
|
|
|
|
|
|
|
|
template = self.get_object()
|
2024-08-07 14:44:18 +02:00
|
|
|
return template.generate_document(body, body_type, export_format)
|
2024-04-03 18:50:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TemplateAccessViewSet(
|
|
|
|
|
ResourceAccessViewsetMixin,
|
|
|
|
|
mixins.CreateModelMixin,
|
|
|
|
|
mixins.DestroyModelMixin,
|
|
|
|
|
mixins.ListModelMixin,
|
|
|
|
|
mixins.RetrieveModelMixin,
|
|
|
|
|
mixins.UpdateModelMixin,
|
|
|
|
|
viewsets.GenericViewSet,
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
API ViewSet for all interactions with template accesses.
|
|
|
|
|
|
|
|
|
|
GET /api/v1.0/templates/<template_id>/accesses/:<template_access_id>
|
|
|
|
|
Return list of all template accesses related to the logged-in user or one
|
|
|
|
|
template access if an id is provided.
|
|
|
|
|
|
|
|
|
|
POST /api/v1.0/templates/<template_id>/accesses/ with expected data:
|
|
|
|
|
- user: str
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [administrator|editor|reader]
|
2024-04-03 18:50:28 +02:00
|
|
|
Return newly created template access
|
|
|
|
|
|
|
|
|
|
PUT /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [owner|admin|editor|reader]
|
2024-04-03 18:50:28 +02:00
|
|
|
Return updated template access
|
|
|
|
|
|
|
|
|
|
PATCH /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [owner|admin|editor|reader]
|
2024-04-03 18:50:28 +02:00
|
|
|
Return partially updated template access
|
|
|
|
|
|
|
|
|
|
DELETE /api/v1.0/templates/<template_id>/accesses/<template_access_id>/
|
|
|
|
|
Delete targeted template access
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
lookup_field = "pk"
|
|
|
|
|
pagination_class = Pagination
|
|
|
|
|
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
|
|
|
|
queryset = models.TemplateAccess.objects.select_related("user").all()
|
|
|
|
|
resource_field_name = "template"
|
|
|
|
|
serializer_class = serializers.TemplateAccessSerializer
|
2024-05-13 23:31:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvitationViewset(
|
|
|
|
|
mixins.CreateModelMixin,
|
|
|
|
|
mixins.ListModelMixin,
|
|
|
|
|
mixins.RetrieveModelMixin,
|
|
|
|
|
mixins.DestroyModelMixin,
|
|
|
|
|
viewsets.GenericViewSet,
|
|
|
|
|
):
|
|
|
|
|
"""API ViewSet for user invitations to document.
|
|
|
|
|
|
|
|
|
|
GET /api/v1.0/documents/<document_id>/invitations/:<invitation_id>/
|
|
|
|
|
Return list of invitations related to that document or one
|
|
|
|
|
document access if an id is provided.
|
|
|
|
|
|
|
|
|
|
POST /api/v1.0/documents/<document_id>/invitations/ with expected data:
|
|
|
|
|
- email: str
|
2024-05-25 08:15:34 +02:00
|
|
|
- role: str [administrator|editor|reader]
|
2024-05-13 23:31:00 +02:00
|
|
|
Return newly created invitation (issuer and document are automatically set)
|
|
|
|
|
|
|
|
|
|
PUT / PATCH : Not permitted. Instead of updating your invitation,
|
|
|
|
|
delete and create a new one.
|
|
|
|
|
|
|
|
|
|
DELETE /api/v1.0/documents/<document_id>/invitations/<invitation_id>/
|
|
|
|
|
Delete targeted invitation
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
lookup_field = "id"
|
|
|
|
|
pagination_class = Pagination
|
|
|
|
|
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
|
|
|
|
queryset = (
|
|
|
|
|
models.Invitation.objects.all()
|
|
|
|
|
.select_related("document")
|
|
|
|
|
.order_by("-created_at")
|
|
|
|
|
)
|
|
|
|
|
serializer_class = serializers.InvitationSerializer
|
|
|
|
|
|
|
|
|
|
def get_serializer_context(self):
|
|
|
|
|
"""Extra context provided to the serializer class."""
|
|
|
|
|
context = super().get_serializer_context()
|
|
|
|
|
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(document=self.kwargs["resource_id"])
|
|
|
|
|
|
|
|
|
|
if self.action == "list":
|
|
|
|
|
user = self.request.user
|
|
|
|
|
teams = user.get_teams()
|
|
|
|
|
|
|
|
|
|
# Determine which role the logged-in user has in the document
|
|
|
|
|
user_roles_query = (
|
|
|
|
|
models.DocumentAccess.objects.filter(
|
|
|
|
|
Q(user=user) | Q(team__in=teams),
|
|
|
|
|
document=self.kwargs["resource_id"],
|
|
|
|
|
)
|
|
|
|
|
.values("document")
|
|
|
|
|
.annotate(roles_array=ArrayAgg("role"))
|
|
|
|
|
.values("roles_array")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
queryset = (
|
|
|
|
|
# The logged-in user should be part of a document to see its accesses
|
|
|
|
|
queryset.filter(
|
|
|
|
|
Q(document__accesses__user=user)
|
|
|
|
|
| Q(document__accesses__team__in=teams),
|
|
|
|
|
)
|
|
|
|
|
# Abilities are computed based on logged-in user's role and
|
|
|
|
|
# the user role on each document access
|
|
|
|
|
.annotate(user_roles=Subquery(user_roles_query))
|
|
|
|
|
.distinct()
|
|
|
|
|
)
|
|
|
|
|
return queryset
|
2024-08-15 15:38:38 +02:00
|
|
|
|
|
|
|
|
def perform_create(self, serializer):
|
|
|
|
|
"""Save invitation to a document then send an email to the invited user."""
|
|
|
|
|
invitation = serializer.save()
|
|
|
|
|
|
|
|
|
|
language = self.request.headers.get("Content-Language", "en-us")
|
|
|
|
|
email_invitation(language, invitation.email, invitation.document.id)
|