✨(backend) allow users to mark/unmark documents as favorite
A user can now mark/unmark documents as favorite.
This is done via a new action of the document API endpoint:
/api/v1.0/documents/{document_id}/favorite
POST to mark as favorite / DELETE to unmark
This commit is contained in:
committed by
Anthony LC
parent
2c915d53f4
commit
89d9075850
@@ -28,9 +28,10 @@ and this project adheres to
|
|||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- 🌐(backend) add german translation #259
|
- ✨(backend) allow users to mark/unmark documents as favorite #411
|
||||||
- 🌐(frontend) Add German translation #255
|
- 🌐(backend) add German translation #259
|
||||||
- ✨(frontend) Add a broadcast store #387
|
- 🌐(frontend) add German translation #255
|
||||||
|
- ✨(frontend) add a broadcast store #387
|
||||||
- ✨(backend) whitelist pod's IP address #443
|
- ✨(backend) whitelist pod's IP address #443
|
||||||
- ✨(backend) config endpoint #425
|
- ✨(backend) config endpoint #425
|
||||||
- ✨(frontend) config endpoint #424
|
- ✨(frontend) config endpoint #424
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class BaseResourceSerializer(serializers.ModelSerializer):
|
|||||||
class ListDocumentSerializer(BaseResourceSerializer):
|
class ListDocumentSerializer(BaseResourceSerializer):
|
||||||
"""Serialize documents with limited fields for display in lists."""
|
"""Serialize documents with limited fields for display in lists."""
|
||||||
|
|
||||||
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Document
|
model = models.Document
|
||||||
@@ -148,6 +149,7 @@ class ListDocumentSerializer(BaseResourceSerializer):
|
|||||||
"abilities",
|
"abilities",
|
||||||
"content",
|
"content",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
"is_favorite",
|
||||||
"link_role",
|
"link_role",
|
||||||
"link_reach",
|
"link_reach",
|
||||||
"title",
|
"title",
|
||||||
@@ -157,6 +159,7 @@ class ListDocumentSerializer(BaseResourceSerializer):
|
|||||||
"id",
|
"id",
|
||||||
"abilities",
|
"abilities",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
"is_favorite",
|
||||||
"link_role",
|
"link_role",
|
||||||
"link_reach",
|
"link_reach",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
@@ -175,6 +178,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
|||||||
"abilities",
|
"abilities",
|
||||||
"content",
|
"content",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
"is_favorite",
|
||||||
"link_role",
|
"link_role",
|
||||||
"link_reach",
|
"link_reach",
|
||||||
"title",
|
"title",
|
||||||
@@ -184,6 +188,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
|||||||
"id",
|
"id",
|
||||||
"abilities",
|
"abilities",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
"is_avorite",
|
||||||
"link_role",
|
"link_role",
|
||||||
"link_reach",
|
"link_reach",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ from django.contrib.postgres.search import TrigramSimilarity
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
|
Exists,
|
||||||
Min,
|
Min,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
Q,
|
Q,
|
||||||
Subquery,
|
Subquery,
|
||||||
|
Value,
|
||||||
)
|
)
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
@@ -193,42 +195,6 @@ class UserViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ResourceViewsetMixin:
|
|
||||||
"""Mixin with methods common to all resource viewsets that are managed with accesses."""
|
|
||||||
|
|
||||||
filter_backends = [filters.OrderingFilter]
|
|
||||||
ordering_fields = ["created_at", "updated_at", "title"]
|
|
||||||
ordering = ["-created_at"]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Custom queryset to get user related resources."""
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
user = self.request.user
|
|
||||||
|
|
||||||
if not user.is_authenticated:
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
user_roles_query = (
|
|
||||||
self.access_model_class.objects.filter(
|
|
||||||
Q(user=user) | Q(team__in=user.teams),
|
|
||||||
**{self.resource_field_name: OuterRef("pk")},
|
|
||||||
)
|
|
||||||
.values(self.resource_field_name)
|
|
||||||
.annotate(roles_array=ArrayAgg("role"))
|
|
||||||
.values("roles_array")
|
|
||||||
)
|
|
||||||
return queryset.annotate(user_roles=Subquery(user_roles_query)).distinct()
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
"""Set the current user as owner of the newly created object."""
|
|
||||||
obj = serializer.save()
|
|
||||||
self.access_model_class.objects.create(
|
|
||||||
user=self.request.user,
|
|
||||||
role=models.RoleChoices.OWNER,
|
|
||||||
**{self.resource_field_name: obj},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceAccessViewsetMixin:
|
class ResourceAccessViewsetMixin:
|
||||||
"""Mixin with methods common to all access viewsets."""
|
"""Mixin with methods common to all access viewsets."""
|
||||||
|
|
||||||
@@ -338,7 +304,6 @@ class DocumentMetadata(metadata.SimpleMetadata):
|
|||||||
|
|
||||||
|
|
||||||
class DocumentViewSet(
|
class DocumentViewSet(
|
||||||
ResourceViewsetMixin,
|
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
@@ -346,14 +311,14 @@ class DocumentViewSet(
|
|||||||
):
|
):
|
||||||
"""Document ViewSet"""
|
"""Document ViewSet"""
|
||||||
|
|
||||||
access_model_class = models.DocumentAccess
|
filter_backends = [filters.OrderingFilter]
|
||||||
metadata_class = DocumentMetadata
|
metadata_class = DocumentMetadata
|
||||||
ordering = ["-updated_at"]
|
ordering = ["-updated_at"]
|
||||||
|
ordering_fields = ["created_at", "is_favorite", "updated_at", "title"]
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.AccessPermission,
|
permissions.AccessPermission,
|
||||||
]
|
]
|
||||||
queryset = models.Document.objects.all()
|
queryset = models.Document.objects.all()
|
||||||
resource_field_name = "document"
|
|
||||||
serializer_class = serializers.DocumentSerializer
|
serializer_class = serializers.DocumentSerializer
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
@@ -364,6 +329,33 @@ class DocumentViewSet(
|
|||||||
return serializers.ListDocumentSerializer
|
return serializers.ListDocumentSerializer
|
||||||
return self.serializer_class
|
return self.serializer_class
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Optimize queryset to include favorite status for the current user."""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
# If the user is not authenticated, annotate `is_favorite` as False
|
||||||
|
return queryset.annotate(is_favorite=Value(False))
|
||||||
|
|
||||||
|
# Annotate the queryset to indicate if the document is favorited by the current user
|
||||||
|
favorite_exists = models.DocumentFavorite.objects.filter(
|
||||||
|
document_id=OuterRef("pk"), user=user
|
||||||
|
)
|
||||||
|
queryset = queryset.annotate(is_favorite=Exists(favorite_exists))
|
||||||
|
|
||||||
|
# Annotate the queryset with the logged-in user roles
|
||||||
|
user_roles_query = (
|
||||||
|
models.DocumentAccess.objects.filter(
|
||||||
|
Q(user=user) | Q(team__in=user.teams),
|
||||||
|
document_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("document")
|
||||||
|
.annotate(roles_array=ArrayAgg("role"))
|
||||||
|
.values("roles_array")
|
||||||
|
)
|
||||||
|
return queryset.annotate(user_roles=Subquery(user_roles_query)).distinct()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""Restrict resources returned by the list endpoint"""
|
"""Restrict resources returned by the list endpoint"""
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
@@ -411,6 +403,15 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
return drf_response.Response(serializer.data)
|
return drf_response.Response(serializer.data)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Set the current user as owner of the newly created object."""
|
||||||
|
obj = serializer.save()
|
||||||
|
models.DocumentAccess.objects.create(
|
||||||
|
document=obj,
|
||||||
|
user=self.request.user,
|
||||||
|
role=models.RoleChoices.OWNER,
|
||||||
|
)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||||
def versions_list(self, request, *args, **kwargs):
|
def versions_list(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -504,6 +505,43 @@ class DocumentViewSet(
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
return drf_response.Response(serializer.data, status=status.HTTP_200_OK)
|
return drf_response.Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@decorators.action(detail=True, methods=["post", "delete"], url_path="favorite")
|
||||||
|
def favorite(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Mark or unmark the document as a favorite for the logged-in user based on the HTTP method.
|
||||||
|
"""
|
||||||
|
# Check permissions first
|
||||||
|
document = self.get_object()
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
# Try to mark as favorite
|
||||||
|
try:
|
||||||
|
models.DocumentFavorite.objects.create(document=document, user=user)
|
||||||
|
except ValidationError:
|
||||||
|
return drf_response.Response(
|
||||||
|
{"detail": "Document already marked as favorite"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
return drf_response.Response(
|
||||||
|
{"detail": "Document marked as favorite"},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle DELETE method to unmark as favorite
|
||||||
|
deleted, _ = models.DocumentFavorite.objects.filter(
|
||||||
|
document=document, user=user
|
||||||
|
).delete()
|
||||||
|
if deleted:
|
||||||
|
return drf_response.Response(
|
||||||
|
{"detail": "Document unmarked as favorite"},
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
return drf_response.Response(
|
||||||
|
{"detail": "Document was already not marked as favorite"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
|
@decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
|
||||||
def attachment_upload(self, request, *args, **kwargs):
|
def attachment_upload(self, request, *args, **kwargs):
|
||||||
"""Upload a file related to a given document"""
|
"""Upload a file related to a given document"""
|
||||||
@@ -687,7 +725,6 @@ class DocumentAccessViewSet(
|
|||||||
|
|
||||||
|
|
||||||
class TemplateViewSet(
|
class TemplateViewSet(
|
||||||
ResourceViewsetMixin,
|
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
@@ -696,15 +733,35 @@ class TemplateViewSet(
|
|||||||
):
|
):
|
||||||
"""Template ViewSet"""
|
"""Template ViewSet"""
|
||||||
|
|
||||||
|
filter_backends = [filters.OrderingFilter]
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.IsAuthenticatedOrSafe,
|
permissions.IsAuthenticatedOrSafe,
|
||||||
permissions.AccessPermission,
|
permissions.AccessPermission,
|
||||||
]
|
]
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
ordering_fields = ["created_at", "updated_at", "title"]
|
||||||
serializer_class = serializers.TemplateSerializer
|
serializer_class = serializers.TemplateSerializer
|
||||||
access_model_class = models.TemplateAccess
|
|
||||||
resource_field_name = "template"
|
|
||||||
queryset = models.Template.objects.all()
|
queryset = models.Template.objects.all()
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Custom queryset to get user related templates."""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
user_roles_query = (
|
||||||
|
models.TemplateAccess.objects.filter(
|
||||||
|
Q(user=user) | Q(team__in=user.teams),
|
||||||
|
template_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("template")
|
||||||
|
.annotate(roles_array=ArrayAgg("role"))
|
||||||
|
.values("roles_array")
|
||||||
|
)
|
||||||
|
return queryset.annotate(user_roles=Subquery(user_roles_query)).distinct()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""Restrict templates returned by the list endpoint"""
|
"""Restrict templates returned by the list endpoint"""
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
@@ -726,6 +783,15 @@ class TemplateViewSet(
|
|||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return drf_response.Response(serializer.data)
|
return drf_response.Response(serializer.data)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Set the current user as owner of the newly created object."""
|
||||||
|
obj = serializer.save()
|
||||||
|
models.TemplateAccess.objects.create(
|
||||||
|
template=obj,
|
||||||
|
user=self.request.user,
|
||||||
|
role=models.RoleChoices.OWNER,
|
||||||
|
)
|
||||||
|
|
||||||
@decorators.action(
|
@decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
|
|||||||
@@ -80,6 +80,13 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
|||||||
for item in extracted:
|
for item in extracted:
|
||||||
models.LinkTrace.objects.create(document=self, user=item)
|
models.LinkTrace.objects.create(document=self, user=item)
|
||||||
|
|
||||||
|
@factory.post_generation
|
||||||
|
def favorited_by(self, create, extracted, **kwargs):
|
||||||
|
"""Mark document as favorited by a list of users."""
|
||||||
|
if create and extracted:
|
||||||
|
for item in extracted:
|
||||||
|
models.DocumentFavorite.objects.create(document=self, user=item)
|
||||||
|
|
||||||
|
|
||||||
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
||||||
"""Create fake document user accesses for testing."""
|
"""Create fake document user accesses for testing."""
|
||||||
|
|||||||
37
src/backend/core/migrations/0009_add_document_favorite.py
Normal file
37
src/backend/core/migrations/0009_add_document_favorite.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2024-11-08 07:59
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0008_alter_document_link_reach'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DocumentFavorite',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||||
|
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by_users', to='core.document')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Document favorite',
|
||||||
|
'verbose_name_plural': 'Document favorites',
|
||||||
|
'db_table': 'impress_document_favorite',
|
||||||
|
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_document_favorite_user', violation_error_message='This document is already targeted by a favorite relation instance for the same user.')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -518,6 +518,7 @@ class Document(BaseModel):
|
|||||||
"ai_translate": is_owner_or_admin or is_editor,
|
"ai_translate": is_owner_or_admin or is_editor,
|
||||||
"attachment_upload": is_owner_or_admin or is_editor,
|
"attachment_upload": is_owner_or_admin or is_editor,
|
||||||
"destroy": RoleChoices.OWNER in roles,
|
"destroy": RoleChoices.OWNER in roles,
|
||||||
|
"favorite": can_get and user.is_authenticated,
|
||||||
"link_configuration": is_owner_or_admin,
|
"link_configuration": is_owner_or_admin,
|
||||||
"invite_owner": RoleChoices.OWNER in roles,
|
"invite_owner": RoleChoices.OWNER in roles,
|
||||||
"partial_update": is_owner_or_admin or is_editor,
|
"partial_update": is_owner_or_admin or is_editor,
|
||||||
@@ -600,6 +601,37 @@ class LinkTrace(BaseModel):
|
|||||||
return f"{self.user!s} trace on document {self.document!s}"
|
return f"{self.user!s} trace on document {self.document!s}"
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentFavorite(BaseModel):
|
||||||
|
"""Relation model to store a user's favorite documents."""
|
||||||
|
|
||||||
|
document = models.ForeignKey(
|
||||||
|
Document,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="favorited_by_users",
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, related_name="favorite_documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_document_favorite"
|
||||||
|
verbose_name = _("Document favorite")
|
||||||
|
verbose_name_plural = _("Document favorites")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "document"],
|
||||||
|
name="unique_document_favorite_user",
|
||||||
|
violation_error_message=_(
|
||||||
|
"This document is already targeted by a favorite relation instance "
|
||||||
|
"for the same user."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user!s} favorite on document {self.document!s}"
|
||||||
|
|
||||||
|
|
||||||
class DocumentAccess(BaseAccess):
|
class DocumentAccess(BaseAccess):
|
||||||
"""Relation model to give access to a document for a user or a team with a role."""
|
"""Relation model to give access to a document for a user or a team with a role."""
|
||||||
|
|
||||||
|
|||||||
308
src/backend/core/tests/documents/test_api_documents_favorite.py
Normal file
308
src/backend/core/tests/documents/test_api_documents_favorite.py
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"""Test favorite document API endpoint for users in impress's core app."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories, models
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach",
|
||||||
|
[
|
||||||
|
"restricted",
|
||||||
|
"authenticated",
|
||||||
|
"public",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize("method", ["post", "delete"])
|
||||||
|
def test_api_document_favorite_anonymous_user(method, reach):
|
||||||
|
"""Anonymous users should not be able to mark/unmark documents as favorites."""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
|
||||||
|
response = getattr(APIClient(), method)(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/favorite/"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert models.DocumentFavorite.objects.exists() is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach, has_role",
|
||||||
|
[
|
||||||
|
["restricted", True],
|
||||||
|
["authenticated", False],
|
||||||
|
["authenticated", True],
|
||||||
|
["public", False],
|
||||||
|
["public", True],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_favorite_authenticated_post_allowed(reach, has_role):
|
||||||
|
"""Authenticated users should be able to mark a document as favorite using POST."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
if has_role:
|
||||||
|
models.DocumentAccess.objects.create(document=document, user=user)
|
||||||
|
|
||||||
|
# Mark as favorite
|
||||||
|
response = client.post(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json() == {"detail": "Document marked as favorite"}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
|
||||||
|
# Verify document format
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.json()["is_favorite"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_favorite_authenticated_post_forbidden():
|
||||||
|
"""Authenticated users should be able to mark a document as favorite using POST."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Try marking as favorite
|
||||||
|
response = client.post(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert (
|
||||||
|
models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach, has_role",
|
||||||
|
[
|
||||||
|
["restricted", True],
|
||||||
|
["authenticated", False],
|
||||||
|
["authenticated", True],
|
||||||
|
["public", False],
|
||||||
|
["public", True],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_favorite_authenticated_post_already_favorited_allowed(
|
||||||
|
reach, has_role
|
||||||
|
):
|
||||||
|
"""POST should not create duplicate favorites if already marked."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, favorited_by=[user])
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
if has_role:
|
||||||
|
models.DocumentAccess.objects.create(document=document, user=user)
|
||||||
|
|
||||||
|
# Try to mark as favorite again
|
||||||
|
response = client.post(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"detail": "Document already marked as favorite"}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
|
||||||
|
# Verify document format
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.json()["is_favorite"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_favorite_authenticated_post_already_favorited_forbidden():
|
||||||
|
"""POST should not create duplicate favorites if already marked."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted", favorited_by=[user])
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Try to mark as favorite again
|
||||||
|
response = client.post(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach, has_role",
|
||||||
|
[
|
||||||
|
["restricted", True],
|
||||||
|
["authenticated", False],
|
||||||
|
["authenticated", True],
|
||||||
|
["public", False],
|
||||||
|
["public", True],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_favorite_authenticated_delete_allowed(reach, has_role):
|
||||||
|
"""Authenticated users should be able to unmark a document as favorite using DELETE."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, favorited_by=[user])
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
if has_role:
|
||||||
|
models.DocumentAccess.objects.create(document=document, user=user)
|
||||||
|
|
||||||
|
# Unmark as favorite
|
||||||
|
response = client.delete(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert (
|
||||||
|
models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify document format
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.json()["is_favorite"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_favorite_authenticated_delete_forbidden():
|
||||||
|
"""Authenticated users should be able to unmark a document as favorite using DELETE."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted", favorited_by=[user])
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Unmark as favorite
|
||||||
|
response = client.delete(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert (
|
||||||
|
models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
is True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach, has_role",
|
||||||
|
[
|
||||||
|
["restricted", True],
|
||||||
|
["authenticated", False],
|
||||||
|
["authenticated", True],
|
||||||
|
["public", False],
|
||||||
|
["public", True],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_favorite_authenticated_delete_not_favorited_allowed(
|
||||||
|
reach, has_role
|
||||||
|
):
|
||||||
|
"""DELETE should be idempotent if the document is not marked as favorite."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
if has_role:
|
||||||
|
models.DocumentAccess.objects.create(document=document, user=user)
|
||||||
|
|
||||||
|
# Try to unmark as favorite when no favorite entry exists
|
||||||
|
response = client.delete(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"detail": "Document was already not marked as favorite"}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert (
|
||||||
|
models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify document format
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.json()["is_favorite"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_favorite_authenticated_delete_not_favorited_forbidden():
|
||||||
|
"""DELETE should be idempotent if the document is not marked as favorite."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Try to unmark as favorite when no favorite entry exists
|
||||||
|
response = client.delete(f"/api/v1.0/documents/{document.id!s}/favorite/")
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert (
|
||||||
|
models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach, has_role",
|
||||||
|
[
|
||||||
|
["restricted", True],
|
||||||
|
["authenticated", False],
|
||||||
|
["authenticated", True],
|
||||||
|
["public", False],
|
||||||
|
["public", True],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_favorite_authenticated_post_unmark_then_mark_again_allowed(
|
||||||
|
reach, has_role
|
||||||
|
):
|
||||||
|
"""A user should be able to mark, unmark, and mark a document again as favorite."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
if has_role:
|
||||||
|
models.DocumentAccess.objects.create(document=document, user=user)
|
||||||
|
|
||||||
|
url = f"/api/v1.0/documents/{document.id!s}/favorite/"
|
||||||
|
|
||||||
|
# Mark as favorite
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
# Unmark as favorite
|
||||||
|
response = client.delete(url)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
# Mark as favorite again
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json() == {"detail": "Document marked as favorite"}
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
assert models.DocumentFavorite.objects.filter(document=document, user=user).exists()
|
||||||
|
|
||||||
|
# Verify document format
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.json()["is_favorite"] is True
|
||||||
@@ -32,6 +32,42 @@ def test_api_documents_list_anonymous(reach, role):
|
|||||||
assert len(results) == 0
|
assert len(results) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_format():
|
||||||
|
"""Validate the format of documents as returned by the list view."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
users=[user, *factories.UserFactory.create_batch(2)],
|
||||||
|
favorited_by=[user, *factories.UserFactory.create_batch(2)],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get("/api/v1.0/documents/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
results = content.pop("results")
|
||||||
|
assert content == {
|
||||||
|
"count": 1,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
}
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0] == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"abilities": document.get_abilities(user),
|
||||||
|
"content": document.content,
|
||||||
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"is_favorite": True,
|
||||||
|
"link_reach": document.link_reach,
|
||||||
|
"link_role": document.link_role,
|
||||||
|
"title": document.title,
|
||||||
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_list_authenticated_direct(django_assert_num_queries):
|
def test_api_documents_list_authenticated_direct(django_assert_num_queries):
|
||||||
"""
|
"""
|
||||||
Authenticated users should be able to list documents they are a direct
|
Authenticated users should be able to list documents they are a direct
|
||||||
@@ -264,6 +300,8 @@ def test_api_documents_list_ordering_by_fields():
|
|||||||
for parameter in [
|
for parameter in [
|
||||||
"created_at",
|
"created_at",
|
||||||
"-created_at",
|
"-created_at",
|
||||||
|
"is_favorite",
|
||||||
|
"-is_favorite",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"-updated_at",
|
"-updated_at",
|
||||||
"title",
|
"title",
|
||||||
@@ -282,3 +320,45 @@ def test_api_documents_list_ordering_by_fields():
|
|||||||
compare = operator.ge if is_descending else operator.le
|
compare = operator.ge if is_descending else operator.le
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
assert compare(results[i][field], results[i + 1][field])
|
assert compare(results[i][field], results[i + 1][field])
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries):
|
||||||
|
"""
|
||||||
|
Ensure that marking documents as favorite does not generate additional queries
|
||||||
|
when fetching the document list.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
special_documents = factories.DocumentFactory.create_batch(3, users=[user])
|
||||||
|
factories.DocumentFactory.create_batch(2, users=[user])
|
||||||
|
|
||||||
|
url = "/api/v1.0/documents/"
|
||||||
|
with django_assert_num_queries(3):
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 5
|
||||||
|
|
||||||
|
assert all(result["is_favorite"] is False for result in results)
|
||||||
|
|
||||||
|
# Mark documents as favorite and check results again
|
||||||
|
for document in special_documents:
|
||||||
|
models.DocumentFavorite.objects.create(document=document, user=user)
|
||||||
|
|
||||||
|
with django_assert_num_queries(3):
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 5
|
||||||
|
|
||||||
|
# Check if the "is_favorite" annotation is correctly set for the favorited documents
|
||||||
|
favorited_ids = {str(doc.id) for doc in special_documents}
|
||||||
|
for result in results:
|
||||||
|
if result["id"] in favorited_ids:
|
||||||
|
assert result["is_favorite"] is True
|
||||||
|
else:
|
||||||
|
assert result["is_favorite"] is False
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ def test_api_documents_retrieve_anonymous_public():
|
|||||||
"ai_translate": document.link_role == "editor",
|
"ai_translate": document.link_role == "editor",
|
||||||
"attachment_upload": document.link_role == "editor",
|
"attachment_upload": document.link_role == "editor",
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
|
# Anonymous user can't favorite a document even with read access
|
||||||
|
"favorite": False,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"partial_update": document.link_role == "editor",
|
"partial_update": document.link_role == "editor",
|
||||||
@@ -38,7 +40,7 @@ def test_api_documents_retrieve_anonymous_public():
|
|||||||
},
|
},
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"is_user_favorite": False,
|
"is_favorite": False,
|
||||||
"link_reach": "public",
|
"link_reach": "public",
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"title": document.title,
|
"title": document.title,
|
||||||
@@ -84,9 +86,10 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"ai_transform": document.link_role == "editor",
|
"ai_transform": document.link_role == "editor",
|
||||||
"ai_translate": document.link_role == "editor",
|
"ai_translate": document.link_role == "editor",
|
||||||
"attachment_upload": document.link_role == "editor",
|
"attachment_upload": document.link_role == "editor",
|
||||||
"link_configuration": False,
|
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": document.link_role == "editor",
|
"partial_update": document.link_role == "editor",
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": document.link_role == "editor",
|
"update": document.link_role == "editor",
|
||||||
@@ -94,12 +97,12 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"versions_list": False,
|
"versions_list": False,
|
||||||
"versions_retrieve": False,
|
"versions_retrieve": False,
|
||||||
},
|
},
|
||||||
"is_user_favorite": False,
|
"content": document.content,
|
||||||
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"is_favorite": False,
|
||||||
"link_reach": reach,
|
"link_reach": reach,
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"title": document.title,
|
"title": document.title,
|
||||||
"content": document.content,
|
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
assert (
|
assert (
|
||||||
@@ -179,12 +182,13 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"title": document.title,
|
|
||||||
"content": document.content,
|
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
|
"content": document.content,
|
||||||
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"is_favorite": False,
|
||||||
"link_reach": document.link_reach,
|
"link_reach": document.link_reach,
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"title": document.title,
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,12 +271,13 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"title": document.title,
|
|
||||||
"content": document.content,
|
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
|
"content": document.content,
|
||||||
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"is_favorite": False,
|
||||||
"link_reach": "restricted",
|
"link_reach": "restricted",
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"title": document.title,
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,12 +325,13 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"title": document.title,
|
|
||||||
"content": document.content,
|
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
|
"content": document.content,
|
||||||
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"is_favorite": False,
|
||||||
"link_reach": "restricted",
|
"link_reach": "restricted",
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"title": document.title,
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,11 +380,12 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"title": document.title,
|
|
||||||
"content": document.content,
|
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
|
"content": document.content,
|
||||||
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"is_favorite": False,
|
||||||
"link_reach": "restricted",
|
"link_reach": "restricted",
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"title": document.title,
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,9 +88,10 @@ def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role)
|
|||||||
"ai_transform": False,
|
"ai_transform": False,
|
||||||
"ai_translate": False,
|
"ai_translate": False,
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
"link_configuration": False,
|
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
|
"favorite": False,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": False,
|
"retrieve": False,
|
||||||
"update": False,
|
"update": False,
|
||||||
@@ -123,8 +124,9 @@ def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
|||||||
"ai_translate": False,
|
"ai_translate": False,
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"link_configuration": False,
|
"favorite": is_authenticated,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": False,
|
||||||
@@ -157,8 +159,9 @@ def test_models_documents_get_abilities_editor(is_authenticated, reach):
|
|||||||
"ai_translate": True,
|
"ai_translate": True,
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"link_configuration": False,
|
"favorite": is_authenticated,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -180,8 +183,9 @@ def test_models_documents_get_abilities_owner():
|
|||||||
"ai_translate": True,
|
"ai_translate": True,
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
"destroy": True,
|
"destroy": True,
|
||||||
"link_configuration": True,
|
"favorite": True,
|
||||||
"invite_owner": True,
|
"invite_owner": True,
|
||||||
|
"link_configuration": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -202,8 +206,9 @@ def test_models_documents_get_abilities_administrator():
|
|||||||
"ai_translate": True,
|
"ai_translate": True,
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"link_configuration": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -227,8 +232,9 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
"ai_translate": True,
|
"ai_translate": True,
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"link_configuration": False,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -254,8 +260,9 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
|
|||||||
"ai_translate": False,
|
"ai_translate": False,
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"link_configuration": False,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": False,
|
||||||
@@ -282,8 +289,9 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
|||||||
"ai_translate": False,
|
"ai_translate": False,
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"link_configuration": False,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": False,
|
||||||
|
|||||||
Reference in New Issue
Block a user