diff --git a/src/backend/core/choices.py b/src/backend/core/choices.py index e6b97511..8505ebab 100644 --- a/src/backend/core/choices.py +++ b/src/backend/core/choices.py @@ -33,6 +33,7 @@ class LinkRoleChoices(PriorityTextChoices): """Defines the possible roles a link can offer on a document.""" READER = "reader", _("Reader") # Can read + COMMENTATOR = "commentator", _("Commentator") # Can read and comment 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.""" READER = "reader", _("Reader") # Can read + COMMENTATOR = "commentator", _("Commentator") # Can read and comment EDITOR = "editor", _("Editor") # Can read and edit ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share OWNER = "owner", _("Owner") diff --git a/src/backend/core/models.py b/src/backend/core/models.py index a1182964..bacef75c 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -762,6 +762,7 @@ class Document(MP_Node, BaseModel): can_update = ( is_owner_or_admin or role == RoleChoices.EDITOR ) 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_access = any( @@ -786,6 +787,7 @@ class Document(MP_Node, BaseModel): "children_list": can_get, "children_create": can_update and user.is_authenticated, "collaboration_auth": can_get, + "comment": can_comment, "cors_proxy": can_get, "descendants": can_get, "destroy": is_owner, @@ -1145,7 +1147,12 @@ class DocumentAccess(BaseAccess): set_role_to = [] if is_owner_or_admin: set_role_to.extend( - [RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN] + [ + RoleChoices.READER, + RoleChoices.COMMENTATOR, + RoleChoices.EDITOR, + RoleChoices.ADMIN, + ] ) if role == RoleChoices.OWNER: set_role_to.append(RoleChoices.OWNER) diff --git a/src/backend/core/tests/documents/test_api_document_accesses.py b/src/backend/core/tests/documents/test_api_document_accesses.py index eb6fa92b..02675e08 100644 --- a/src/backend/core/tests/documents/test_api_document_accesses.py +++ b/src/backend/core/tests/documents/test_api_document_accesses.py @@ -292,6 +292,7 @@ def test_api_document_accesses_retrieve_set_role_to_child(): } assert result_dict[str(document_access_other_user.id)] == [ "reader", + "commentator", "editor", "administrator", "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 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/") @@ -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 } assert result_dict[str(document_access_other_user.id)] == [ + "commentator", "editor", "administrator", "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_other_user.id)] == [ "reader", + "commentator", "editor", "administrator", "owner", @@ -332,28 +335,28 @@ def test_api_document_accesses_retrieve_set_role_to_child(): [ ["administrator", "reader", "reader", "reader"], [ - ["reader", "editor", "administrator"], + ["reader", "commentator", "editor", "administrator"], [], [], - ["reader", "editor", "administrator"], + ["reader", "commentator", "editor", "administrator"], ], ], [ ["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"], [ - ["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"], [ - ["reader", "editor", "administrator"], + ["reader", "commentator", "editor", "administrator"], [], [], - ["reader", "editor", "administrator"], + ["reader", "commentator", "editor", "administrator"], ], ], [ ["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"], [ - ["reader", "editor", "administrator", "owner"], + ["reader", "commentator", "editor", "administrator", "owner"], [], [], - ["reader", "editor", "administrator", "owner"], + ["reader", "commentator", "editor", "administrator", "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", "editor", "administrator"], - ["reader", "editor", "administrator"], + ["reader", "commentator", "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"], [ - ["reader", "editor", "administrator"], + ["reader", "commentator", "editor", "administrator"], [], ["editor", "administrator"], [], diff --git a/src/backend/core/tests/documents/test_api_documents_retrieve.py b/src/backend/core/tests/documents/test_api_documents_retrieve.py index 63fa8f09..eb235842 100644 --- a/src/backend/core/tests/documents/test_api_documents_retrieve.py +++ b/src/backend/core/tests/documents/test_api_documents_retrieve.py @@ -36,6 +36,7 @@ def test_api_documents_retrieve_anonymous_public_standalone(): "children_create": False, "children_list": True, "collaboration_auth": True, + "comment": document.link_role in ["commentator", "editor"], "cors_proxy": True, "descendants": True, "destroy": False, @@ -45,8 +46,8 @@ def test_api_documents_retrieve_anonymous_public_standalone(): "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": False, @@ -111,6 +112,7 @@ def test_api_documents_retrieve_anonymous_public_parent(): "children_create": False, "children_list": True, "collaboration_auth": True, + "comment": grand_parent.link_role in ["commentator", "editor"], "descendants": True, "cors_proxy": True, "destroy": False, @@ -216,6 +218,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated( "children_create": document.link_role == "editor", "children_list": True, "collaboration_auth": True, + "comment": document.link_role in ["commentator", "editor"], "descendants": True, "cors_proxy": True, "destroy": False, @@ -224,8 +227,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated( "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "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_list": True, "collaboration_auth": True, + "comment": grand_parent.link_role in ["commentator", "editor"], "descendants": True, "cors_proxy": True, "destroy": False, @@ -488,10 +492,11 @@ def test_api_documents_retrieve_authenticated_related_parent(): "ai_transform": access.role != "reader", "ai_translate": 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_list": True, "collaboration_auth": True, + "comment": access.role != "reader", "descendants": True, "cors_proxy": True, "destroy": access.role == "owner", diff --git a/src/backend/core/tests/documents/test_api_documents_trashbin.py b/src/backend/core/tests/documents/test_api_documents_trashbin.py index 25bc1cda..b3c5c35b 100644 --- a/src/backend/core/tests/documents/test_api_documents_trashbin.py +++ b/src/backend/core/tests/documents/test_api_documents_trashbin.py @@ -79,16 +79,17 @@ def test_api_documents_trashbin_format(): "children_create": True, "children_list": True, "collaboration_auth": True, - "descendants": True, + "comment": True, "cors_proxy": True, + "descendants": True, "destroy": True, "duplicate": True, "favorite": True, "invite_owner": True, "link_configuration": True, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": True, diff --git a/src/backend/core/tests/test_models_document_accesses.py b/src/backend/core/tests/test_models_document_accesses.py index 2fa88cf1..eb7675c0 100644 --- a/src/backend/core/tests/test_models_document_accesses.py +++ b/src/backend/core/tests/test_models_document_accesses.py @@ -123,7 +123,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed(): "retrieve": True, "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, "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, "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, "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, "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, "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, "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, "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, "update": True, "partial_update": True, - "set_role_to": ["reader", "editor", "administrator"], + "set_role_to": ["reader", "commentator", "editor", "administrator"], } diff --git a/src/backend/core/tests/test_models_documents.py b/src/backend/core/tests/test_models_documents.py index 6874009c..0bedd160 100644 --- a/src/backend/core/tests/test_models_documents.py +++ b/src/backend/core/tests/test_models_documents.py @@ -134,10 +134,13 @@ def test_models_documents_soft_delete(depth): [ (True, "restricted", "reader"), (True, "restricted", "editor"), + (True, "restricted", "commentator"), (False, "restricted", "reader"), (False, "restricted", "editor"), + (False, "restricted", "commentator"), (False, "authenticated", "reader"), (False, "authenticated", "editor"), + (False, "authenticated", "commentator"), ], ) def test_models_documents_get_abilities_forbidden( @@ -164,6 +167,7 @@ def test_models_documents_get_abilities_forbidden( "destroy": False, "duplicate": False, "favorite": False, + "comment": False, "invite_owner": False, "mask": False, "media_auth": False, @@ -171,8 +175,8 @@ def test_models_documents_get_abilities_forbidden( "move": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "partial_update": False, @@ -222,6 +226,7 @@ def test_models_documents_get_abilities_reader( "children_create": False, "children_list": True, "collaboration_auth": True, + "comment": False, "descendants": True, "cors_proxy": True, "destroy": False, @@ -230,8 +235,77 @@ def test_models_documents_get_abilities_reader( "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "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, }, "mask": is_authenticated, @@ -287,6 +361,7 @@ def test_models_documents_get_abilities_editor( "children_create": is_authenticated, "children_list": True, "collaboration_auth": True, + "comment": True, "descendants": True, "cors_proxy": True, "destroy": False, @@ -295,8 +370,8 @@ def test_models_documents_get_abilities_editor( "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": is_authenticated, @@ -341,6 +416,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries): "children_create": True, "children_list": True, "collaboration_auth": True, + "comment": True, "descendants": True, "cors_proxy": True, "destroy": True, @@ -349,8 +425,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries): "invite_owner": True, "link_configuration": True, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": True, @@ -392,6 +468,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries) "children_create": True, "children_list": True, "collaboration_auth": True, + "comment": True, "descendants": True, "cors_proxy": True, "destroy": False, @@ -400,8 +477,8 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries) "invite_owner": False, "link_configuration": True, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": True, @@ -446,6 +523,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries): "children_create": True, "children_list": True, "collaboration_auth": True, + "comment": True, "descendants": True, "cors_proxy": True, "destroy": False, @@ -454,8 +532,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries): "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": True, @@ -507,6 +585,8 @@ def test_models_documents_get_abilities_reader_user( "children_create": access_from_link, "children_list": True, "collaboration_auth": True, + "comment": document.link_reach != "restricted" + and document.link_role in ["commentator", "editor"], "descendants": True, "cors_proxy": True, "destroy": False, @@ -515,8 +595,72 @@ def test_models_documents_get_abilities_reader_user( "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "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, }, "mask": True, @@ -566,6 +710,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries): "children_create": False, "children_list": True, "collaboration_auth": True, + "comment": False, "descendants": True, "cors_proxy": True, "destroy": False, @@ -574,8 +719,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries): "invite_owner": False, "link_configuration": False, "link_select_options": { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "editor"], + "public": ["reader", "commentator", "editor"], "restricted": None, }, "mask": True, @@ -1198,7 +1343,14 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries): "public", "reader", { - "public": ["reader", "editor"], + "public": ["reader", "commentator", "editor"], + }, + ), + ( + "public", + "commentator", + { + "public": ["commentator", "editor"], }, ), ("public", "editor", {"public": ["editor"]}), @@ -1206,8 +1358,16 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries): "authenticated", "reader", { - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "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", { "restricted": None, - "authenticated": ["reader", "editor"], - "public": ["reader", "editor"], + "authenticated": ["reader", "commentator", "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", None, { - "public": ["reader", "editor"], + "public": ["reader", "commentator", "editor"], }, ), ( None, "reader", { - "public": ["reader", "editor"], - "authenticated": ["reader", "editor"], + "public": ["reader", "commentator", "editor"], + "authenticated": ["reader", "commentator", "editor"], "restricted": None, }, ), @@ -1254,8 +1423,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries): None, None, { - "public": ["reader", "editor"], - "authenticated": ["reader", "editor"], + "public": ["reader", "commentator", "editor"], + "authenticated": ["reader", "commentator", "editor"], "restricted": None, }, ),