Compare commits

...

3 Commits

Author SHA1 Message Date
Manuel Raynaud
f51246b392 (backend) add comment viewset
This commit add the CRUD part to manage comment lifeycle. Permissions
are relying on the Document and Comment abilities. Comment viewset
depends on the Document route and is added to the
document_related_router. Dedicated serializer and permission are
created.
2025-08-28 08:26:16 +02:00
Manuel Raynaud
a11a1911bc (backend) add Comment model
In order to store the comments on a document, we created a new model
Comment. User is nullable because anonymous users can comment a Document
is this one is public with a link_role commentator.
2025-08-27 16:38:42 +02:00
Manuel Raynaud
604e5e0eb2 (backend) add commentator role
To allow a user to comment a document we added a new role: commentator.
Commentator is higher than reader but lower than editor.
2025-08-26 17:55:53 +02:00
16 changed files with 1411 additions and 63 deletions

View File

@@ -8,6 +8,10 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Added
- ✨(backend) Comments on text editor #1309
### Changed ### Changed
- ⚡️(frontend) improve accessibility: - ⚡️(frontend) improve accessibility:

View File

@@ -171,3 +171,19 @@ class ResourceAccessPermission(IsAuthenticated):
action = view.action action = view.action
return abilities.get(action, False) return abilities.get(action, False)
class CommentPermission(permissions.BasePermission):
"""Permission class for comments."""
def has_permission(self, request, view):
"""Check permission for a given object."""
if view.action in ["create", "list"]:
document_abilities = view.get_document_or_404().get_abilities(request.user)
return document_abilities["comment"]
return True
def has_object_permission(self, request, view, obj):
"""Check permission for a given object."""
return obj.get_abilities(request.user).get(view.action, False)

View File

@@ -801,3 +801,47 @@ class MoveDocumentSerializer(serializers.Serializer):
choices=enums.MoveNodePositionChoices.choices, choices=enums.MoveNodePositionChoices.choices,
default=enums.MoveNodePositionChoices.LAST_CHILD, default=enums.MoveNodePositionChoices.LAST_CHILD,
) )
class CommentSerializer(serializers.ModelSerializer):
"""Serialize comments."""
user = UserLightSerializer(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.Comment
fields = [
"id",
"content",
"created_at",
"updated_at",
"user",
"document",
"abilities",
]
read_only_fields = [
"id",
"created_at",
"updated_at",
"user",
"document",
"abilities",
]
def get_abilities(self, comment) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return comment.get_abilities(request.user)
return {}
def validate(self, attrs):
"""Validate invitation data."""
request = self.context.get("request")
user = getattr(request, "user", None)
attrs["document_id"] = self.context["resource_id"]
attrs["user_id"] = user.id if user else None
return attrs

View File

@@ -2072,3 +2072,36 @@ class ConfigView(drf.views.APIView):
) )
return theme_customization return theme_customization
class CommentViewSet(
viewsets.ModelViewSet,
):
"""API ViewSet for comments."""
permission_classes = [permissions.CommentPermission]
queryset = models.Comment.objects.select_related("user", "document").all()
serializer_class = serializers.CommentSerializer
pagination_class = Pagination
_document = None
def get_document_or_404(self):
"""Get the document related to the viewset or raise a 404 error."""
if self._document is None:
try:
self._document = models.Document.objects.get(
pk=self.kwargs["resource_id"],
)
except models.Document.DoesNotExist as e:
raise drf.exceptions.NotFound("Document not found.") from e
return self._document
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."""
return super().get_queryset().filter(document=self.kwargs["resource_id"])

View File

@@ -33,6 +33,7 @@ class LinkRoleChoices(PriorityTextChoices):
"""Defines the possible roles a link can offer on a document.""" """Defines the possible roles a link can offer on a document."""
READER = "reader", _("Reader") # Can read READER = "reader", _("Reader") # Can read
COMMENTATOR = "commentator", _("Commentator") # Can read and comment
EDITOR = "editor", _("Editor") # Can read and edit EDITOR = "editor", _("Editor") # Can read and edit
@@ -40,6 +41,7 @@ class RoleChoices(PriorityTextChoices):
"""Defines the possible roles a user can have in a resource.""" """Defines the possible roles a user can have in a resource."""
READER = "reader", _("Reader") # Can read READER = "reader", _("Reader") # Can read
COMMENTATOR = "commentator", _("Commentator") # Can read and comment
EDITOR = "editor", _("Editor") # Can read and edit EDITOR = "editor", _("Editor") # Can read and edit
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
OWNER = "owner", _("Owner") OWNER = "owner", _("Owner")

View File

@@ -256,3 +256,14 @@ class InvitationFactory(factory.django.DjangoModelFactory):
document = factory.SubFactory(DocumentFactory) document = factory.SubFactory(DocumentFactory)
role = factory.fuzzy.FuzzyChoice([role[0] for role in models.RoleChoices.choices]) role = factory.fuzzy.FuzzyChoice([role[0] for role in models.RoleChoices.choices])
issuer = factory.SubFactory(UserFactory) issuer = factory.SubFactory(UserFactory)
class CommentFactory(factory.django.DjangoModelFactory):
"""A factory to create comments for a document"""
class Meta:
model = models.Comment
document = factory.SubFactory(DocumentFactory)
user = factory.SubFactory(UserFactory)
content = factory.Faker("text")

View File

@@ -0,0 +1,146 @@
# Generated by Django 5.2.4 on 2025-08-26 08:11
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0024_add_is_masked_field_to_link_trace"),
]
operations = [
migrations.AlterField(
model_name="document",
name="link_role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="documentaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="documentaskforaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="invitation",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="templateaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.CreateModel(
name="Comment",
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",
),
),
("content", models.TextField()),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="core.document",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="comments",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Comment",
"verbose_name_plural": "Comments",
"db_table": "impress_comment",
"ordering": ("-created_at",),
},
),
]

View File

@@ -762,6 +762,7 @@ class Document(MP_Node, BaseModel):
can_update = ( can_update = (
is_owner_or_admin or role == RoleChoices.EDITOR is_owner_or_admin or role == RoleChoices.EDITOR
) and not is_deleted ) and not is_deleted
can_comment = (can_update or role == RoleChoices.COMMENTATOR) and not is_deleted
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any( ai_access = any(
@@ -786,6 +787,7 @@ class Document(MP_Node, BaseModel):
"children_list": can_get, "children_list": can_get,
"children_create": can_update and user.is_authenticated, "children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get, "collaboration_auth": can_get,
"comment": can_comment,
"cors_proxy": can_get, "cors_proxy": can_get,
"descendants": can_get, "descendants": can_get,
"destroy": is_owner, "destroy": is_owner,
@@ -1145,7 +1147,12 @@ class DocumentAccess(BaseAccess):
set_role_to = [] set_role_to = []
if is_owner_or_admin: if is_owner_or_admin:
set_role_to.extend( set_role_to.extend(
[RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN] [
RoleChoices.READER,
RoleChoices.COMMENTATOR,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]
) )
if role == RoleChoices.OWNER: if role == RoleChoices.OWNER:
set_role_to.append(RoleChoices.OWNER) set_role_to.append(RoleChoices.OWNER)
@@ -1277,6 +1284,48 @@ class DocumentAskForAccess(BaseModel):
self.document.send_email(subject, [email], context, language) self.document.send_email(subject, [email], context, language)
class Comment(BaseModel):
"""User comment on a document."""
document = models.ForeignKey(
Document,
on_delete=models.CASCADE,
related_name="comments",
)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="comments",
null=True,
blank=True,
)
content = models.TextField()
class Meta:
db_table = "impress_comment"
ordering = ("-created_at",)
verbose_name = _("Comment")
verbose_name_plural = _("Comments")
def __str__(self):
author = self.user or _("Anonymous")
return f"{author!s} on {self.document!s}"
def get_abilities(self, user):
"""Compute and return abilities for a given user."""
role = self.document.get_role(user)
can_comment = self.document.get_abilities(user)["comment"]
return {
"destroy": self.user == user
or role in [RoleChoices.OWNER, RoleChoices.ADMIN],
"update": self.user == user
or role in [RoleChoices.OWNER, RoleChoices.ADMIN],
"partial_update": self.user == user
or role in [RoleChoices.OWNER, RoleChoices.ADMIN],
"retrieve": can_comment,
}
class Template(BaseModel): class Template(BaseModel):
"""HTML and CSS code used for formatting the print around the MarkDown body.""" """HTML and CSS code used for formatting the print around the MarkDown body."""

View File

@@ -292,6 +292,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
} }
assert result_dict[str(document_access_other_user.id)] == [ assert result_dict[str(document_access_other_user.id)] == [
"reader", "reader",
"commentator",
"editor", "editor",
"administrator", "administrator",
"owner", "owner",
@@ -300,7 +301,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
# Add an access for the other user on the parent # Add an access for the other user on the parent
parent_access_other_user = factories.UserDocumentAccessFactory( parent_access_other_user = factories.UserDocumentAccessFactory(
document=parent, user=other_user, role="editor" document=parent, user=other_user, role="commentator"
) )
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/") response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
@@ -313,6 +314,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
result["id"]: result["abilities"]["set_role_to"] for result in content result["id"]: result["abilities"]["set_role_to"] for result in content
} }
assert result_dict[str(document_access_other_user.id)] == [ assert result_dict[str(document_access_other_user.id)] == [
"commentator",
"editor", "editor",
"administrator", "administrator",
"owner", "owner",
@@ -320,6 +322,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
assert result_dict[str(parent_access.id)] == [] assert result_dict[str(parent_access.id)] == []
assert result_dict[str(parent_access_other_user.id)] == [ assert result_dict[str(parent_access_other_user.id)] == [
"reader", "reader",
"commentator",
"editor", "editor",
"administrator", "administrator",
"owner", "owner",
@@ -332,28 +335,28 @@ def test_api_document_accesses_retrieve_set_role_to_child():
[ [
["administrator", "reader", "reader", "reader"], ["administrator", "reader", "reader", "reader"],
[ [
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
[], [],
[], [],
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
], ],
], ],
[ [
["owner", "reader", "reader", "reader"], ["owner", "reader", "reader", "reader"],
[ [
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
[], [],
[], [],
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
], ],
], ],
[ [
["owner", "reader", "reader", "owner"], ["owner", "reader", "reader", "owner"],
[ [
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
[], [],
[], [],
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
], ],
], ],
], ],
@@ -414,44 +417,44 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
[ [
["administrator", "reader", "reader", "reader"], ["administrator", "reader", "reader", "reader"],
[ [
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
[], [],
[], [],
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
], ],
], ],
[ [
["owner", "reader", "reader", "reader"], ["owner", "reader", "reader", "reader"],
[ [
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
[], [],
[], [],
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
], ],
], ],
[ [
["owner", "reader", "reader", "owner"], ["owner", "reader", "reader", "owner"],
[ [
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
[], [],
[], [],
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
], ],
], ],
[ [
["reader", "reader", "reader", "owner"], ["reader", "reader", "reader", "owner"],
[ [
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
[], [],
[], [],
["reader", "editor", "administrator", "owner"], ["reader", "commentator", "editor", "administrator", "owner"],
], ],
], ],
[ [
["reader", "administrator", "reader", "editor"], ["reader", "administrator", "reader", "editor"],
[ [
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
[], [],
[], [],
], ],
@@ -459,7 +462,7 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
[ [
["editor", "editor", "administrator", "editor"], ["editor", "editor", "administrator", "editor"],
[ [
["reader", "editor", "administrator"], ["reader", "commentator", "editor", "administrator"],
[], [],
["editor", "administrator"], ["editor", "administrator"],
[], [],

View File

@@ -0,0 +1,588 @@
"""Test API for comments on documents."""
import random
from django.contrib.auth.models import AnonymousUser
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
# List comments
def test_list_comments_anonymous_user_public_document():
"""Anonymous users should be allowed to list comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment1, comment2 = factories.CommentFactory.create_batch(2, document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 200
assert response.json() == {
"count": 2,
"next": None,
"previous": None,
"results": [
{
"id": str(comment2.id),
"content": comment2.content,
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment2.user.full_name,
"short_name": comment2.user.short_name,
},
"document": str(comment2.document.id),
"abilities": comment2.get_abilities(AnonymousUser()),
},
{
"id": str(comment1.id),
"content": comment1.content,
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment1.user.full_name,
"short_name": comment1.user.short_name,
},
"document": str(comment1.document.id),
"abilities": comment1.get_abilities(AnonymousUser()),
},
],
}
@pytest.mark.parametrize("link_reach", ["restricted", "authenticated"])
def test_list_comments_anonymous_user_non_public_document(link_reach):
"""Anonymous users should not be allowed to list comments on a non-public document."""
document = factories.DocumentFactory(
link_reach=link_reach, link_role=models.LinkRoleChoices.COMMENTATOR
)
factories.CommentFactory(document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 401
def test_list_comments_authenticated_user_accessible_document():
"""Authenticated users should be allowed to list comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment1 = factories.CommentFactory(document=document)
comment2 = factories.CommentFactory(document=document, user=user)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 200
assert response.json() == {
"count": 2,
"next": None,
"previous": None,
"results": [
{
"id": str(comment2.id),
"content": comment2.content,
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment2.user.full_name,
"short_name": comment2.user.short_name,
},
"document": str(comment2.document.id),
"abilities": comment2.get_abilities(user),
},
{
"id": str(comment1.id),
"content": comment1.content,
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment1.user.full_name,
"short_name": comment1.user.short_name,
},
"document": str(comment1.document.id),
"abilities": comment1.get_abilities(user),
},
],
}
def test_list_comments_authenticated_user_non_accessible_document():
"""Authenticated users should not be allowed to list comments on a non-accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
factories.CommentFactory(document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 403
def test_list_comments_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to list comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
factories.CommentFactory(document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 403
# Create comment
def test_create_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to create comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 201
assert response.json() == {
"id": str(response.json()["id"]),
"content": "test",
"created_at": response.json()["created_at"],
"updated_at": response.json()["updated_at"],
"user": None,
"document": str(document.id),
"abilities": {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": True,
},
}
def test_create_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to create comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 401
def test_create_comment_authenticated_user_accessible_document():
"""Authenticated users should be allowed to create comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 201
assert response.json() == {
"id": str(response.json()["id"]),
"content": "test",
"created_at": response.json()["created_at"],
"updated_at": response.json()["updated_at"],
"user": {
"full_name": user.full_name,
"short_name": user.short_name,
},
"document": str(document.id),
"abilities": {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
},
}
def test_create_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to create comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 403
# Retrieve comment
def test_retrieve_comment_anonymous_user_public_document():
"""Anonymous users should be allowed to retrieve comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 200
assert response.json() == {
"id": str(comment.id),
"content": comment.content,
"created_at": comment.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment.user.full_name,
"short_name": comment.user.short_name,
},
"document": str(comment.document.id),
"abilities": comment.get_abilities(AnonymousUser()),
}
def test_retrieve_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to retrieve comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_retrieve_comment_authenticated_user_accessible_document():
"""Authenticated users should be allowed to retrieve comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 200
def test_retrieve_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to retrieve comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
# Update comment
def test_update_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to update comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 401
def test_update_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to update comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 401
def test_update_comment_authenticated_user_accessible_document():
"""Authenticated users should not be able to update comments not their own."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted",
users=[
(
user,
random.choice(
[models.LinkRoleChoices.COMMENTATOR, models.LinkRoleChoices.EDITOR]
),
)
],
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 403
def test_update_comment_authenticated_user_own_comment():
"""Authenticated users should be able to update comments not their own."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted",
users=[
(
user,
random.choice(
[models.LinkRoleChoices.COMMENTATOR, models.LinkRoleChoices.EDITOR]
),
)
],
)
comment = factories.CommentFactory(document=document, content="test", user=user)
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.content == "other content"
def test_update_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to update comments on a document they don't
have comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 403
def test_update_comment_authenticated_no_access():
"""
Authenticated users should not be allowed to update comments on a document they don't
have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 403
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_any_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.content == "other content"
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_own_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document, content="test", user=user)
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.content == "other content"
# Delete comment
def test_delete_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to delete comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_delete_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to delete comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_delete_comment_authenticated_user_accessible_document_own_comment():
"""Authenticated users should be able to delete comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment = factories.CommentFactory(document=document, user=user)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
def test_delete_comment_authenticated_user_accessible_document_not_own_comment():
"""Authenticated users should not be able to delete comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_any_comment(role):
"""Authenticated users should be able to delete comments on a document they have access to."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_own_comment(role):
"""Authenticated users should be able to delete comments on a document they have access to."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document, user=user)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
def test_delete_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be able to delete comments on a document they don't
have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403

View File

@@ -36,6 +36,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"children_create": False, "children_create": False,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": document.link_role in ["commentator", "editor"],
"cors_proxy": True, "cors_proxy": True,
"descendants": True, "descendants": True,
"destroy": False, "destroy": False,
@@ -45,8 +46,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": False, "mask": False,
@@ -111,6 +112,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"children_create": False, "children_create": False,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": grand_parent.link_role in ["commentator", "editor"],
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -216,6 +218,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"children_create": document.link_role == "editor", "children_create": document.link_role == "editor",
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": document.link_role in ["commentator", "editor"],
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -224,8 +227,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,
@@ -298,6 +301,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"children_create": grand_parent.link_role == "editor", "children_create": grand_parent.link_role == "editor",
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": grand_parent.link_role in ["commentator", "editor"],
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -488,10 +492,11 @@ def test_api_documents_retrieve_authenticated_related_parent():
"ai_transform": access.role != "reader", "ai_transform": access.role != "reader",
"ai_translate": access.role != "reader", "ai_translate": access.role != "reader",
"attachment_upload": access.role != "reader", "attachment_upload": access.role != "reader",
"can_edit": access.role != "reader", "can_edit": access.role not in ["reader", "commentator"],
"children_create": access.role != "reader", "children_create": access.role != "reader",
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": access.role != "reader",
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": access.role == "owner", "destroy": access.role == "owner",

View File

@@ -79,16 +79,17 @@ def test_api_documents_trashbin_format():
"children_create": True, "children_create": True,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"descendants": True, "comment": True,
"cors_proxy": True, "cors_proxy": True,
"descendants": True,
"destroy": True, "destroy": True,
"duplicate": True, "duplicate": True,
"favorite": True, "favorite": True,
"invite_owner": True, "invite_owner": True,
"link_configuration": True, "link_configuration": True,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,

View File

@@ -0,0 +1,273 @@
"""Test the comment model."""
import random
from django.contrib.auth.models import AnonymousUser
import pytest
from core import factories
from core.models import LinkReachChoices, LinkRoleChoices, RoleChoices
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"role,can_comment",
[
(LinkRoleChoices.READER, False),
(LinkRoleChoices.COMMENTATOR, True),
(LinkRoleChoices.EDITOR, True),
],
)
def test_comment_get_abilities_anonymous_user_public_document(role, can_comment):
"""Anonymous users cannot comment on a document."""
document = factories.DocumentFactory(
link_role=role, link_reach=LinkReachChoices.PUBLIC
)
comment = factories.CommentFactory(document=document)
user = AnonymousUser()
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": can_comment,
}
@pytest.mark.parametrize(
"link_reach", [LinkReachChoices.RESTRICTED, LinkReachChoices.AUTHENTICATED]
)
def test_comment_get_abilities_anonymous_user_restricted_document(link_reach):
"""Anonymous users cannot comment on a restricted document."""
document = factories.DocumentFactory(link_reach=link_reach)
comment = factories.CommentFactory(document=document)
user = AnonymousUser()
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": False,
}
@pytest.mark.parametrize(
"link_role,link_reach,can_comment",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED, True),
],
)
def test_comment_get_abilities_user_reader(link_role, link_reach, can_comment):
"""Readers cannot comment on a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.READER)]
)
comment = factories.CommentFactory(document=document)
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": can_comment,
}
@pytest.mark.parametrize(
"link_role,link_reach,can_comment",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED, True),
],
)
def test_comment_get_abilities_user_reader_own_comment(
link_role, link_reach, can_comment
):
"""User with reader role on a document has all accesses to its own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.READER)]
)
comment = factories.CommentFactory(
document=document, user=user if can_comment else None
)
assert comment.get_abilities(user) == {
"destroy": can_comment,
"update": can_comment,
"partial_update": can_comment,
"retrieve": can_comment,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_commentator(link_role, link_reach):
"""Commentators can comment on a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role,
link_reach=link_reach,
users=[(user, RoleChoices.COMMENTATOR)],
)
comment = factories.CommentFactory(document=document)
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": True,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_commentator_own_comment(link_role, link_reach):
"""Commentators have all accesses to its own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role,
link_reach=link_reach,
users=[(user, RoleChoices.COMMENTATOR)],
)
comment = factories.CommentFactory(document=document, user=user)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_editor(link_role, link_reach):
"""Editors can comment on a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.EDITOR)]
)
comment = factories.CommentFactory(document=document)
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": True,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_editor_own_comment(link_role, link_reach):
"""Editors have all accesses to its own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.EDITOR)]
)
comment = factories.CommentFactory(document=document, user=user)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
def test_comment_get_abilities_user_admin():
"""Admins have all accesses to a comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, RoleChoices.ADMIN)])
comment = factories.CommentFactory(
document=document, user=random.choice([user, None])
)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
def test_comment_get_abilities_user_owner():
"""Owners have all accesses to a comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, RoleChoices.OWNER)])
comment = factories.CommentFactory(
document=document, user=random.choice([user, None])
)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}

View File

@@ -123,7 +123,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator", "owner"], "set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
} }
@@ -166,7 +166,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_last_on_child(
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator", "owner"], "set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
} }
@@ -183,7 +183,7 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator", "owner"], "set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
} }
@@ -200,7 +200,7 @@ def test_models_document_access_get_abilities_for_owner_of_administrator():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator", "owner"], "set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
} }
@@ -217,7 +217,7 @@ def test_models_document_access_get_abilities_for_owner_of_editor():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator", "owner"], "set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
} }
@@ -234,7 +234,7 @@ def test_models_document_access_get_abilities_for_owner_of_reader():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator", "owner"], "set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
} }
@@ -271,7 +271,7 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator"], "set_role_to": ["reader", "commentator", "editor", "administrator"],
} }
@@ -288,7 +288,7 @@ def test_models_document_access_get_abilities_for_administrator_of_editor():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator"], "set_role_to": ["reader", "commentator", "editor", "administrator"],
} }
@@ -305,7 +305,7 @@ def test_models_document_access_get_abilities_for_administrator_of_reader():
"retrieve": True, "retrieve": True,
"update": True, "update": True,
"partial_update": True, "partial_update": True,
"set_role_to": ["reader", "editor", "administrator"], "set_role_to": ["reader", "commentator", "editor", "administrator"],
} }

View File

@@ -134,10 +134,13 @@ def test_models_documents_soft_delete(depth):
[ [
(True, "restricted", "reader"), (True, "restricted", "reader"),
(True, "restricted", "editor"), (True, "restricted", "editor"),
(True, "restricted", "commentator"),
(False, "restricted", "reader"), (False, "restricted", "reader"),
(False, "restricted", "editor"), (False, "restricted", "editor"),
(False, "restricted", "commentator"),
(False, "authenticated", "reader"), (False, "authenticated", "reader"),
(False, "authenticated", "editor"), (False, "authenticated", "editor"),
(False, "authenticated", "commentator"),
], ],
) )
def test_models_documents_get_abilities_forbidden( def test_models_documents_get_abilities_forbidden(
@@ -164,6 +167,7 @@ def test_models_documents_get_abilities_forbidden(
"destroy": False, "destroy": False,
"duplicate": False, "duplicate": False,
"favorite": False, "favorite": False,
"comment": False,
"invite_owner": False, "invite_owner": False,
"mask": False, "mask": False,
"media_auth": False, "media_auth": False,
@@ -171,8 +175,8 @@ def test_models_documents_get_abilities_forbidden(
"move": False, "move": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"partial_update": False, "partial_update": False,
@@ -222,6 +226,7 @@ def test_models_documents_get_abilities_reader(
"children_create": False, "children_create": False,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": False,
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -230,8 +235,77 @@ def test_models_documents_get_abilities_reader(
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,
"retrieve": True,
"tree": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
}
nb_queries = 1 if is_authenticated else 0
with django_assert_num_queries(nb_queries):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(
value is False
for key, value in document.get_abilities(user).items()
if key not in ["link_select_options", "ancestors_links_definition"]
)
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_documents_get_abilities_commentator(
is_authenticated, reach, django_assert_num_queries
):
"""
Check abilities returned for a document giving commentator role to link holders
i.e anonymous users or authenticated users who have no specific role on the document.
"""
document = factories.DocumentFactory(link_reach=reach, link_role="commentator")
user = factories.UserFactory() if is_authenticated else AnonymousUser()
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": is_authenticated, "mask": is_authenticated,
@@ -287,6 +361,7 @@ def test_models_documents_get_abilities_editor(
"children_create": is_authenticated, "children_create": is_authenticated,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": True,
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -295,8 +370,8 @@ def test_models_documents_get_abilities_editor(
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": is_authenticated, "mask": is_authenticated,
@@ -341,6 +416,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"children_create": True, "children_create": True,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": True,
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": True, "destroy": True,
@@ -349,8 +425,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"invite_owner": True, "invite_owner": True,
"link_configuration": True, "link_configuration": True,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,
@@ -392,6 +468,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"children_create": True, "children_create": True,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": True,
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -400,8 +477,8 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"invite_owner": False, "invite_owner": False,
"link_configuration": True, "link_configuration": True,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,
@@ -446,6 +523,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"children_create": True, "children_create": True,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": True,
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -454,8 +532,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,
@@ -507,6 +585,8 @@ def test_models_documents_get_abilities_reader_user(
"children_create": access_from_link, "children_create": access_from_link,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": document.link_reach != "restricted"
and document.link_role in ["commentator", "editor"],
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -515,8 +595,72 @@ def test_models_documents_get_abilities_reader_user(
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": access_from_link,
"restore": False,
"retrieve": True,
"tree": True,
"update": access_from_link,
"versions_destroy": False,
"versions_list": True,
"versions_retrieve": True,
}
with override_settings(AI_ALLOW_REACH_FROM=ai_access_setting):
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(
value is False
for key, value in document.get_abilities(user).items()
if key not in ["link_select_options", "ancestors_links_definition"]
)
@pytest.mark.parametrize("ai_access_setting", ["public", "authenticated", "restricted"])
def test_models_documents_get_abilities_commentator_user(
ai_access_setting, django_assert_num_queries
):
"""Check abilities returned for the commentator of a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "commentator")])
access_from_link = (
document.link_reach != "restricted" and document.link_role == "editor"
)
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
"can_edit": access_from_link,
"children_create": access_from_link,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": True,
"favorite": True,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,
@@ -566,6 +710,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"children_create": False, "children_create": False,
"children_list": True, "children_list": True,
"collaboration_auth": True, "collaboration_auth": True,
"comment": False,
"descendants": True, "descendants": True,
"cors_proxy": True, "cors_proxy": True,
"destroy": False, "destroy": False,
@@ -574,8 +719,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"invite_owner": False, "invite_owner": False,
"link_configuration": False, "link_configuration": False,
"link_select_options": { "link_select_options": {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
"mask": True, "mask": True,
@@ -1198,7 +1343,14 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"public", "public",
"reader", "reader",
{ {
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
},
),
(
"public",
"commentator",
{
"public": ["commentator", "editor"],
}, },
), ),
("public", "editor", {"public": ["editor"]}), ("public", "editor", {"public": ["editor"]}),
@@ -1206,8 +1358,16 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"authenticated", "authenticated",
"reader", "reader",
{ {
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
},
),
(
"authenticated",
"commentator",
{
"authenticated": ["commentator", "editor"],
"public": ["commentator", "editor"],
}, },
), ),
( (
@@ -1220,8 +1380,17 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"reader", "reader",
{ {
"restricted": None, "restricted": None,
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
},
),
(
"restricted",
"commentator",
{
"restricted": None,
"authenticated": ["commentator", "editor"],
"public": ["commentator", "editor"],
}, },
), ),
( (
@@ -1238,15 +1407,15 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"public", "public",
None, None,
{ {
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
}, },
), ),
( (
None, None,
"reader", "reader",
{ {
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
), ),
@@ -1254,8 +1423,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
None, None,
None, None,
{ {
"public": ["reader", "editor"], "public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"], "authenticated": ["reader", "commentator", "editor"],
"restricted": None, "restricted": None,
}, },
), ),

View File

@@ -26,7 +26,11 @@ document_related_router.register(
viewsets.InvitationViewset, viewsets.InvitationViewset,
basename="invitations", basename="invitations",
) )
document_related_router.register(
"comments",
viewsets.CommentViewSet,
basename="comments",
)
document_related_router.register( document_related_router.register(
"ask-for-access", "ask-for-access",
viewsets.DocumentAskForAccessViewSet, viewsets.DocumentAskForAccessViewSet,