From 926fe37e85db68f42bb29c7931a4a08de028cc8a Mon Sep 17 00:00:00 2001 From: Samuel Paccoud - DINUM Date: Sat, 25 May 2024 08:15:34 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(models)=20rename=20document/?= =?UTF-8?q?template=20access=20rights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "member" access right does not make sense for documents and templates. What we really need are "editor" and "reader" access rights. --- src/backend/core/admin.py | 3 +- src/backend/core/api/viewsets.py | 15 +- src/backend/core/migrations/0001_initial.py | 27 +++- ...alter_user_language_invitation_and_more.py | 42 ----- src/backend/core/models.py | 34 +++-- .../documents/test_api_documents_delete.py | 8 +- .../documents/test_api_documents_retrieve.py | 93 ++++++++--- .../documents/test_api_documents_update.py | 14 +- .../templates/test_api_templates_delete.py | 2 +- .../templates/test_api_templates_retrieve.py | 93 ++++++++--- .../templates/test_api_templates_update.py | 13 +- .../core/tests/test_api_document_accesses.py | 45 +++--- .../tests/test_api_document_invitations.py | 36 +++-- .../core/tests/test_api_document_versions.py | 9 +- .../core/tests/test_api_template_accesses.py | 45 +++--- .../tests/test_models_document_accesses.py | 144 ++++++++++++++---- .../core/tests/test_models_documents.py | 29 +++- .../core/tests/test_models_invitations.py | 37 ++++- .../tests/test_models_template_accesses.py | 144 ++++++++++++++---- .../core/tests/test_models_templates.py | 27 +++- .../features/pads/pad-management/types.tsx | 3 +- .../src/features/pads/pad-tools/types.ts | 3 +- 22 files changed, 601 insertions(+), 265 deletions(-) delete mode 100644 src/backend/core/migrations/0002_alter_user_language_invitation_and_more.py diff --git a/src/backend/core/admin.py b/src/backend/core/admin.py index 4e95d933..f9a50422 100644 --- a/src/backend/core/admin.py +++ b/src/backend/core/admin.py @@ -91,7 +91,7 @@ class DocumentAdmin(admin.ModelAdmin): """Document admin interface declaration.""" inlines = (DocumentAccessInline,) - + @admin.register(models.Invitation) class InvitationAdmin(admin.ModelAdmin): @@ -119,4 +119,3 @@ class InvitationAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): obj.issuer = request.user obj.save() - diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 1b5e16b0..dd7611df 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1,5 +1,4 @@ """API endpoints""" -import json from io import BytesIO from django.contrib.postgres.aggregates import ArrayAgg @@ -368,15 +367,15 @@ class DocumentAccessViewSet( POST /api/v1.0/documents//accesses/ with expected data: - user: str - - role: str [owner|admin|member] + - role: str [administrator|editor|reader] Return newly created document access PUT /api/v1.0/documents//accesses// with expected data: - - role: str [owner|admin|member] + - role: str [owner|admin|editor|reader] Return updated document access PATCH /api/v1.0/documents//accesses// with expected data: - - role: str [owner|admin|member] + - role: str [owner|admin|editor|reader] Return partially updated document access DELETE /api/v1.0/documents//accesses// @@ -458,15 +457,15 @@ class TemplateAccessViewSet( POST /api/v1.0/templates//accesses/ with expected data: - user: str - - role: str [owner|admin|member] + - role: str [administrator|editor|reader] Return newly created template access PUT /api/v1.0/templates//accesses// with expected data: - - role: str [owner|admin|member] + - role: str [owner|admin|editor|reader] Return updated template access PATCH /api/v1.0/templates//accesses// with expected data: - - role: str [owner|admin|member] + - role: str [owner|admin|editor|reader] Return partially updated template access DELETE /api/v1.0/templates//accesses// @@ -496,7 +495,7 @@ class InvitationViewset( POST /api/v1.0/documents//invitations/ with expected data: - email: str - - role: str [owner|admin|member] + - role: str [administrator|editor|reader] Return newly created invitation (issuer and document are automatically set) PUT / PATCH : Not permitted. Instead of updating your invitation, diff --git a/src/backend/core/migrations/0001_initial.py b/src/backend/core/migrations/0001_initial.py index a01a03d2..38bdb4f3 100644 --- a/src/backend/core/migrations/0001_initial.py +++ b/src/backend/core/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.3 on 2024-04-19 11:38 +# Generated by Django 5.0.3 on 2024-05-28 20:29 import django.contrib.auth.models import django.core.validators @@ -89,7 +89,7 @@ class Migration(migrations.Migration): ('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')), ('team', models.CharField(blank=True, max_length=100)), - ('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)), + ('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)), ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.document')), ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -100,6 +100,23 @@ class Migration(migrations.Migration): 'ordering': ('-created_at',), }, ), + migrations.CreateModel( + name='Invitation', + 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')), + ('email', models.EmailField(max_length=254, verbose_name='email address')), + ('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')), + ('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Document invitation', + 'verbose_name_plural': 'Document invitations', + 'db_table': 'impress_invitation', + }, + ), migrations.CreateModel( name='TemplateAccess', fields=[ @@ -107,7 +124,7 @@ class Migration(migrations.Migration): ('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')), ('team', models.CharField(blank=True, max_length=100)), - ('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)), + ('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)), ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')), ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -130,6 +147,10 @@ class Migration(migrations.Migration): model_name='documentaccess', constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'), ), + migrations.AddConstraint( + model_name='invitation', + constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'), + ), migrations.AddConstraint( model_name='templateaccess', constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'), diff --git a/src/backend/core/migrations/0002_alter_user_language_invitation_and_more.py b/src/backend/core/migrations/0002_alter_user_language_invitation_and_more.py deleted file mode 100644 index dd066fa1..00000000 --- a/src/backend/core/migrations/0002_alter_user_language_invitation_and_more.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 5.0.3 on 2024-05-12 19:02 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", 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='Invitation', - 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')), - ('email', models.EmailField(max_length=254, verbose_name='email address')), - ('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')), - ('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Document invitation', - 'verbose_name_plural': 'Document invitations', - 'db_table': 'impress_invitation', - }, - ), - migrations.AddConstraint( - model_name='invitation', - constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'), - ), - ] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 390ebee0..c0434536 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -2,7 +2,6 @@ Declare and configure the models for the impress core application """ import hashlib -import json import smtplib import textwrap import uuid @@ -55,8 +54,9 @@ def get_resource_roles(resource, user): class RoleChoices(models.TextChoices): """Defines the possible roles a user can have in a template.""" - MEMBER = "member", _("Member") - ADMIN = "administrator", _("Administrator") + READER = "reader", _("Reader") # Can read + EDITOR = "editor", _("Editor") # Can read and edit + ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share OWNER = "owner", _("Owner") @@ -233,7 +233,7 @@ class BaseAccess(BaseModel): ) team = models.CharField(max_length=100, blank=True) role = models.CharField( - max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER + max_length=20, choices=RoleChoices.choices, default=RoleChoices.READER ) class Meta: @@ -265,14 +265,20 @@ class BaseAccess(BaseModel): RoleChoices.OWNER in roles and resource.accesses.filter(role=RoleChoices.OWNER).count() > 1 ) - set_role_to = [RoleChoices.ADMIN, RoleChoices.MEMBER] if can_delete else [] + set_role_to = ( + [RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER] + if can_delete + else [] + ) else: can_delete = is_owner_or_admin set_role_to = [] if RoleChoices.OWNER in roles: set_role_to.append(RoleChoices.OWNER) if is_owner_or_admin: - set_role_to.extend([RoleChoices.ADMIN, RoleChoices.MEMBER]) + set_role_to.extend( + [RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER] + ) # Remove the current role as we don't want to propose it as an option try: @@ -325,7 +331,7 @@ class Document(BaseModel): except (FileNotFoundError, ClientError): pass else: - self._content = response["Body"].read().decode('utf-8') + self._content = response["Body"].read().decode("utf-8") return self._content @content.setter @@ -333,7 +339,7 @@ class Document(BaseModel): """Cache the content, don't write to object storage yet""" if not isinstance(content, str): raise ValueError("content should be a string.") - + self._content = content def get_content_response(self, version_id=""): @@ -447,6 +453,7 @@ class Document(BaseModel): is_owner_or_admin = bool( set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN}) ) + is_editor = bool(RoleChoices.EDITOR in roles) can_get = self.is_public or bool(roles) can_get_versions = bool(roles) @@ -456,8 +463,8 @@ class Document(BaseModel): "versions_list": can_get_versions, "versions_retrieve": can_get_versions, "manage_accesses": is_owner_or_admin, - "update": is_owner_or_admin, - "partial_update": is_owner_or_admin, + "update": is_owner_or_admin or is_editor, + "partial_update": is_owner_or_admin or is_editor, "retrieve": can_get, } @@ -537,14 +544,15 @@ class Template(BaseModel): is_owner_or_admin = bool( set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN}) ) + is_editor = bool(RoleChoices.EDITOR in roles) can_get = self.is_public or bool(roles) return { "destroy": RoleChoices.OWNER in roles, "generate_document": can_get, "manage_accesses": is_owner_or_admin, - "update": is_owner_or_admin, - "partial_update": is_owner_or_admin, + "update": is_owner_or_admin or is_editor, + "partial_update": is_owner_or_admin or is_editor, "retrieve": can_get, } @@ -631,7 +639,7 @@ class Invitation(BaseModel): related_name="invitations", ) role = models.CharField( - max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER + max_length=20, choices=RoleChoices.choices, default=RoleChoices.READER ) issuer = models.ForeignKey( User, diff --git a/src/backend/core/tests/documents/test_api_documents_delete.py b/src/backend/core/tests/documents/test_api_documents_delete.py index d0a5ab1e..7cf67bd2 100644 --- a/src/backend/core/tests/documents/test_api_documents_delete.py +++ b/src/backend/core/tests/documents/test_api_documents_delete.py @@ -45,14 +45,12 @@ def test_api_documents_delete_authenticated_unrelated(): assert models.Document.objects.count() == 1 -@pytest.mark.parametrize("role", ["member", "administrator"]) +@pytest.mark.parametrize("role", ["reader", "editor", "administrator"]) @pytest.mark.parametrize("via", VIA) -def test_api_documents_delete_authenticated_member_or_administrator( - via, role, mock_user_get_teams -): +def test_api_documents_delete_authenticated_not_owner(via, role, mock_user_get_teams): """ Authenticated users should not be allowed to delete a document for which they are - only a member or administrator. + only a reader, editor or administrator. """ user = factories.UserFactory() 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 e995040e..e3e81eff 100644 --- a/src/backend/core/tests/documents/test_api_documents_retrieve.py +++ b/src/backend/core/tests/documents/test_api_documents_retrieve.py @@ -161,7 +161,10 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te document = factories.DocumentFactory(is_public=False) factories.TeamDocumentAccessFactory( - document=document, team="members", role="member" + document=document, team="readers", role="reader" + ) + factories.TeamDocumentAccessFactory( + document=document, team="editors", role="editor" ) factories.TeamDocumentAccessFactory( document=document, team="administrators", role="administrator" @@ -178,8 +181,10 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te @pytest.mark.parametrize( "teams", [ - ["members"], - ["unknown", "members"], + ["readers"], + ["unknown", "readers"], + ["editors"], + ["unknown", "editors"], ], ) def test_api_documents_retrieve_authenticated_related_team_members( @@ -198,8 +203,11 @@ def test_api_documents_retrieve_authenticated_related_team_members( document = factories.DocumentFactory(is_public=False) - access_member = factories.TeamDocumentAccessFactory( - document=document, team="members", role="member" + access_reader = factories.TeamDocumentAccessFactory( + document=document, team="readers", role="reader" + ) + access_editor = factories.TeamDocumentAccessFactory( + document=document, team="editors", role="editor" ) access_administrator = factories.TeamDocumentAccessFactory( document=document, team="administrators", role="administrator" @@ -222,10 +230,17 @@ def test_api_documents_retrieve_authenticated_related_team_members( assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted( [ { - "id": str(access_member.id), + "id": str(access_reader.id), "user": None, - "team": "members", - "role": access_member.role, + "team": "readers", + "role": access_reader.role, + "abilities": expected_abilities, + }, + { + "id": str(access_editor.id), + "user": None, + "team": "editors", + "role": access_editor.role, "abilities": expected_abilities, }, { @@ -265,7 +280,7 @@ def test_api_documents_retrieve_authenticated_related_team_members( "teams", [ ["administrators"], - ["members", "administrators"], + ["editors", "administrators"], ["unknown", "administrators"], ], ) @@ -285,8 +300,11 @@ def test_api_documents_retrieve_authenticated_related_team_administrators( document = factories.DocumentFactory(is_public=False) - access_member = factories.TeamDocumentAccessFactory( - document=document, team="members", role="member" + access_reader = factories.TeamDocumentAccessFactory( + document=document, team="readers", role="reader" + ) + access_editor = factories.TeamDocumentAccessFactory( + document=document, team="editors", role="editor" ) access_administrator = factories.TeamDocumentAccessFactory( document=document, team="administrators", role="administrator" @@ -305,14 +323,26 @@ def test_api_documents_retrieve_authenticated_related_team_administrators( assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted( [ { - "id": str(access_member.id), + "id": str(access_reader.id), "user": None, - "team": "members", - "role": "member", + "team": "readers", + "role": "reader", "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["administrator"], + "set_role_to": ["administrator", "editor"], + "update": True, + }, + }, + { + "id": str(access_editor.id), + "user": None, + "team": "editors", + "role": "editor", + "abilities": { + "destroy": True, + "retrieve": True, + "set_role_to": ["administrator", "reader"], "update": True, }, }, @@ -324,7 +354,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators( "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["member"], + "set_role_to": ["editor", "reader"], "update": True, }, }, @@ -384,8 +414,11 @@ def test_api_documents_retrieve_authenticated_related_team_owners( document = factories.DocumentFactory(is_public=False) - access_member = factories.TeamDocumentAccessFactory( - document=document, team="members", role="member" + access_reader = factories.TeamDocumentAccessFactory( + document=document, team="readers", role="reader" + ) + access_editor = factories.TeamDocumentAccessFactory( + document=document, team="editors", role="editor" ) access_administrator = factories.TeamDocumentAccessFactory( document=document, team="administrators", role="administrator" @@ -404,14 +437,26 @@ def test_api_documents_retrieve_authenticated_related_team_owners( assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted( [ { - "id": str(access_member.id), + "id": str(access_reader.id), "user": None, - "team": "members", - "role": "member", + "team": "readers", + "role": "reader", "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["owner", "administrator"], + "set_role_to": ["owner", "administrator", "editor"], + "update": True, + }, + }, + { + "id": str(access_editor.id), + "user": None, + "team": "editors", + "role": "editor", + "abilities": { + "destroy": True, + "retrieve": True, + "set_role_to": ["owner", "administrator", "reader"], "update": True, }, }, @@ -423,7 +468,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners( "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["owner", "member"], + "set_role_to": ["owner", "editor", "reader"], "update": True, }, }, @@ -436,7 +481,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners( # editable only if there is another owner role than the user's team... "destroy": other_access.role == "owner", "retrieve": True, - "set_role_to": ["administrator", "member"] + "set_role_to": ["administrator", "editor", "reader"] if other_access.role == "owner" else [], "update": other_access.role == "owner", diff --git a/src/backend/core/tests/documents/test_api_documents_update.py b/src/backend/core/tests/documents/test_api_documents_update.py index fb2981c6..9fc5414a 100644 --- a/src/backend/core/tests/documents/test_api_documents_update.py +++ b/src/backend/core/tests/documents/test_api_documents_update.py @@ -66,9 +66,9 @@ def test_api_documents_update_authenticated_unrelated(): @pytest.mark.parametrize("via", VIA) -def test_api_documents_update_authenticated_members(via, mock_user_get_teams): +def test_api_documents_update_authenticated_reader(via, mock_user_get_teams): """ - Users who are members of a document but not administrators should + Users who are editors or reader of a document but not administrators should not be allowed to update it. """ user = factories.UserFactory() @@ -78,11 +78,11 @@ def test_api_documents_update_authenticated_members(via, mock_user_get_teams): document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role="reader") elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role="reader" ) old_document_values = serializers.DocumentSerializer(instance=document).data @@ -106,12 +106,12 @@ def test_api_documents_update_authenticated_members(via, mock_user_get_teams): assert document_values == old_document_values -@pytest.mark.parametrize("role", ["administrator", "owner"]) +@pytest.mark.parametrize("role", ["editor", "administrator", "owner"]) @pytest.mark.parametrize("via", VIA) -def test_api_documents_update_authenticated_administrator_or_owner( +def test_api_documents_update_authenticated_editor_administrator_or_owner( via, role, mock_user_get_teams ): - """Administrator or owner of a document should be allowed to update it.""" + """A user who is editor, administrator or owner of a document should be allowed to update it.""" user = factories.UserFactory() client = APIClient() diff --git a/src/backend/core/tests/templates/test_api_templates_delete.py b/src/backend/core/tests/templates/test_api_templates_delete.py index 166b6646..5a8f32cd 100644 --- a/src/backend/core/tests/templates/test_api_templates_delete.py +++ b/src/backend/core/tests/templates/test_api_templates_delete.py @@ -45,7 +45,7 @@ def test_api_templates_delete_authenticated_unrelated(): assert models.Template.objects.count() == 1 -@pytest.mark.parametrize("role", ["member", "administrator"]) +@pytest.mark.parametrize("role", ["reader", "editor", "administrator"]) @pytest.mark.parametrize("via", VIA) def test_api_templates_delete_authenticated_member_or_administrator( via, role, mock_user_get_teams diff --git a/src/backend/core/tests/templates/test_api_templates_retrieve.py b/src/backend/core/tests/templates/test_api_templates_retrieve.py index 0993f414..e83bdda0 100644 --- a/src/backend/core/tests/templates/test_api_templates_retrieve.py +++ b/src/backend/core/tests/templates/test_api_templates_retrieve.py @@ -160,7 +160,10 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te template = factories.TemplateFactory(is_public=False) factories.TeamTemplateAccessFactory( - template=template, team="members", role="member" + template=template, team="readers", role="reader" + ) + factories.TeamTemplateAccessFactory( + template=template, team="editors", role="editor" ) factories.TeamTemplateAccessFactory( template=template, team="administrators", role="administrator" @@ -177,11 +180,13 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te @pytest.mark.parametrize( "teams", [ - ["members"], - ["unknown", "members"], + ["readers"], + ["unknown", "readers"], + ["editors"], + ["unknown", "editors"], ], ) -def test_api_templates_retrieve_authenticated_related_team_members( +def test_api_templates_retrieve_authenticated_related_team_readers_or_editors( teams, mock_user_get_teams ): """ @@ -197,8 +202,11 @@ def test_api_templates_retrieve_authenticated_related_team_members( template = factories.TemplateFactory(is_public=False) - access_member = factories.TeamTemplateAccessFactory( - template=template, team="members", role="member" + access_reader = factories.TeamTemplateAccessFactory( + template=template, team="readers", role="reader" + ) + access_editor = factories.TeamTemplateAccessFactory( + template=template, team="editors", role="editor" ) access_administrator = factories.TeamTemplateAccessFactory( template=template, team="administrators", role="administrator" @@ -221,10 +229,17 @@ def test_api_templates_retrieve_authenticated_related_team_members( assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted( [ { - "id": str(access_member.id), + "id": str(access_reader.id), "user": None, - "team": "members", - "role": access_member.role, + "team": "readers", + "role": access_reader.role, + "abilities": expected_abilities, + }, + { + "id": str(access_editor.id), + "user": None, + "team": "editors", + "role": access_editor.role, "abilities": expected_abilities, }, { @@ -285,8 +300,11 @@ def test_api_templates_retrieve_authenticated_related_team_administrators( template = factories.TemplateFactory(is_public=False) - access_member = factories.TeamTemplateAccessFactory( - template=template, team="members", role="member" + access_reader = factories.TeamTemplateAccessFactory( + template=template, team="readers", role="reader" + ) + access_editor = factories.TeamTemplateAccessFactory( + template=template, team="editors", role="editor" ) access_administrator = factories.TeamTemplateAccessFactory( template=template, team="administrators", role="administrator" @@ -304,14 +322,26 @@ def test_api_templates_retrieve_authenticated_related_team_administrators( assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted( [ { - "id": str(access_member.id), + "id": str(access_reader.id), "user": None, - "team": "members", - "role": "member", + "team": "readers", + "role": "reader", "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["administrator"], + "set_role_to": ["administrator", "editor"], + "update": True, + }, + }, + { + "id": str(access_editor.id), + "user": None, + "team": "editors", + "role": "editor", + "abilities": { + "destroy": True, + "retrieve": True, + "set_role_to": ["administrator", "reader"], "update": True, }, }, @@ -323,7 +353,7 @@ def test_api_templates_retrieve_authenticated_related_team_administrators( "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["member"], + "set_role_to": ["editor", "reader"], "update": True, }, }, @@ -384,8 +414,11 @@ def test_api_templates_retrieve_authenticated_related_team_owners( template = factories.TemplateFactory(is_public=False) - access_member = factories.TeamTemplateAccessFactory( - template=template, team="members", role="member" + access_reader = factories.TeamTemplateAccessFactory( + template=template, team="readers", role="reader" + ) + access_editor = factories.TeamTemplateAccessFactory( + template=template, team="editors", role="editor" ) access_administrator = factories.TeamTemplateAccessFactory( template=template, team="administrators", role="administrator" @@ -403,14 +436,26 @@ def test_api_templates_retrieve_authenticated_related_team_owners( assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted( [ { - "id": str(access_member.id), + "id": str(access_reader.id), "user": None, - "team": "members", - "role": "member", + "team": "readers", + "role": "reader", "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["owner", "administrator"], + "set_role_to": ["owner", "administrator", "editor"], + "update": True, + }, + }, + { + "id": str(access_editor.id), + "user": None, + "team": "editors", + "role": "editor", + "abilities": { + "destroy": True, + "retrieve": True, + "set_role_to": ["owner", "administrator", "reader"], "update": True, }, }, @@ -422,7 +467,7 @@ def test_api_templates_retrieve_authenticated_related_team_owners( "abilities": { "destroy": True, "retrieve": True, - "set_role_to": ["owner", "member"], + "set_role_to": ["owner", "editor", "reader"], "update": True, }, }, @@ -435,7 +480,7 @@ def test_api_templates_retrieve_authenticated_related_team_owners( # editable only if there is another owner role than the user's team... "destroy": other_access.role == "owner", "retrieve": True, - "set_role_to": ["administrator", "member"] + "set_role_to": ["administrator", "editor", "reader"] if other_access.role == "owner" else [], "update": other_access.role == "owner", diff --git a/src/backend/core/tests/templates/test_api_templates_update.py b/src/backend/core/tests/templates/test_api_templates_update.py index 92fa5415..89a82299 100644 --- a/src/backend/core/tests/templates/test_api_templates_update.py +++ b/src/backend/core/tests/templates/test_api_templates_update.py @@ -66,10 +66,9 @@ def test_api_templates_update_authenticated_unrelated(): @pytest.mark.parametrize("via", VIA) -def test_api_templates_update_authenticated_members(via, mock_user_get_teams): +def test_api_templates_update_authenticated_readers(via, mock_user_get_teams): """ - Users who are members of a template but not administrators should - not be allowed to update it. + Users who are readers of a template should not be allowed to update it. """ user = factories.UserFactory() @@ -78,11 +77,11 @@ def test_api_templates_update_authenticated_members(via, mock_user_get_teams): template = factories.TemplateFactory() if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="member") + factories.UserTemplateAccessFactory(template=template, user=user, role="reader") elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="member" + template=template, team="lasuite", role="reader" ) old_template_values = serializers.TemplateSerializer(instance=template).data @@ -106,9 +105,9 @@ def test_api_templates_update_authenticated_members(via, mock_user_get_teams): assert template_values == old_template_values -@pytest.mark.parametrize("role", ["administrator", "owner"]) +@pytest.mark.parametrize("role", ["editor", "administrator", "owner"]) @pytest.mark.parametrize("via", VIA) -def test_api_templates_update_authenticated_administrator_or_owner( +def test_api_templates_update_authenticated_editor_or_administrator_or_owner( via, role, mock_user_get_teams ): """Administrator or owner of a template should be allowed to update it.""" diff --git a/src/backend/core/tests/test_api_document_accesses.py b/src/backend/core/tests/test_api_document_accesses.py index e5299049..7251529a 100644 --- a/src/backend/core/tests/test_api_document_accesses.py +++ b/src/backend/core/tests/test_api_document_accesses.py @@ -254,9 +254,12 @@ def test_api_document_accesses_create_authenticated_unrelated(): assert not models.DocumentAccess.objects.filter(user=other_user).exists() +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_create_authenticated_member(via, mock_user_get_teams): - """Members of a document should not be allowed to create document accesses.""" +def test_api_document_accesses_create_authenticated_reader_or_editor( + via, role, mock_user_get_teams +): + """Readers or editors of a document should not be allowed to create document accesses.""" user = factories.UserFactory() client = APIClient() @@ -264,21 +267,21 @@ def test_api_document_accesses_create_authenticated_member(via, mock_user_get_te document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role=role ) other_user = factories.UserFactory() - for role in [role[0] for role in models.RoleChoices.choices]: + for new_role in [role[0] for role in models.RoleChoices.choices]: response = client.post( f"/api/v1.0/documents/{document.id!s}/accesses/", { "user": str(other_user.id), - "role": role, + "role": new_role, }, format="json", ) @@ -456,9 +459,12 @@ def test_api_document_accesses_update_authenticated_unrelated(): assert updated_values == old_values +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_update_authenticated_member(via, mock_user_get_teams): - """Members of a document should not be allowed to update its accesses.""" +def test_api_document_accesses_update_authenticated_reader_or_editor( + via, role, mock_user_get_teams +): + """Readers or editors of a document should not be allowed to update its accesses.""" user = factories.UserFactory() client = APIClient() @@ -466,11 +472,11 @@ def test_api_document_accesses_update_authenticated_member(via, mock_user_get_te document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role=role ) access = factories.UserDocumentAccessFactory(document=document) @@ -521,14 +527,14 @@ def test_api_document_accesses_update_administrator_except_owner( access = factories.UserDocumentAccessFactory( document=document, - role=random.choice(["administrator", "member"]), + role=random.choice(["administrator", "editor", "reader"]), ) old_values = serializers.DocumentAccessSerializer(instance=access).data new_values = { "id": uuid4(), "user_id": factories.UserFactory().id, - "role": random.choice(["administrator", "member"]), + "role": random.choice(["administrator", "editor", "reader"]), } for field, value in new_values.items(): @@ -629,7 +635,7 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_get_ access = factories.UserDocumentAccessFactory( document=document, user=other_user, - role=random.choice(["administrator", "member"]), + role=random.choice(["administrator", "editor", "reader"]), ) old_values = serializers.DocumentAccessSerializer(instance=access).data @@ -736,7 +742,7 @@ def test_api_document_accesses_update_owner_self(via, mock_user_get_teams): ) old_values = serializers.DocumentAccessSerializer(instance=access).data - new_role = random.choice(["administrator", "member"]) + new_role = random.choice(["administrator", "editor", "reader"]) response = client.put( f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", @@ -797,11 +803,12 @@ def test_api_document_accesses_delete_authenticated(): assert models.DocumentAccess.objects.count() == 1 +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_delete_member(via, mock_user_get_teams): +def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_get_teams): """ Authenticated users should not be allowed to delete a document access for a - document in which they are a simple member. + document in which they are a simple reader or editor. """ user = factories.UserFactory() @@ -810,11 +817,11 @@ def test_api_document_accesses_delete_member(via, mock_user_get_teams): document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role=role ) access = factories.UserDocumentAccessFactory(document=document) @@ -855,7 +862,7 @@ def test_api_document_accesses_delete_administrators_except_owners( ) access = factories.UserDocumentAccessFactory( - document=document, role=random.choice(["member", "administrator"]) + document=document, role=random.choice(["reader", "editor", "administrator"]) ) assert models.DocumentAccess.objects.count() == 2 diff --git a/src/backend/core/tests/test_api_document_invitations.py b/src/backend/core/tests/test_api_document_invitations.py index a5f2f227..7d95fce0 100644 --- a/src/backend/core/tests/test_api_document_invitations.py +++ b/src/backend/core/tests/test_api_document_invitations.py @@ -57,13 +57,20 @@ def test_api_document_invitations__create__authenticated_outsider(): @pytest.mark.parametrize( "inviting,invited,is_allowed", ( - ["member", "member", False], - ["member", "administrator", False], - ["member", "owner", False], - ["administrator", "member", True], + ["reader", "reader", False], + ["reader", "editor", False], + ["reader", "administrator", False], + ["reader", "owner", False], + ["editor", "reader", False], + ["editor", "editor", False], + ["editor", "administrator", False], + ["editor", "owner", False], + ["administrator", "reader", True], + ["administrator", "editor", True], ["administrator", "administrator", True], ["administrator", "owner", False], - ["owner", "member", True], + ["owner", "reader", True], + ["owner", "editor", True], ["owner", "administrator", True], ["owner", "owner", True], ), @@ -146,7 +153,7 @@ def test_api_document_invitations__create__cannot_duplicate_invitation(): # Create a new invitation to the same document with the exact same email address invitation_values = { "email": existing_invitation.email, - "role": random.choice(["administrator", "member"]), + "role": random.choice(["administrator", "editor", "reader"]), } client = APIClient() @@ -222,12 +229,12 @@ def test_api_document_invitations__list__authenticated( document=document, role="administrator", issuer=user ) other_invitations = factories.InvitationFactory.create_batch( - 2, document=document, role="member", issuer=other_user + 2, document=document, role="reader", issuer=other_user ) # invitations from other documents should not be listed other_document = factories.DocumentFactory() - factories.InvitationFactory.create_batch(2, document=other_document, role="member") + factories.InvitationFactory.create_batch(2, document=other_document, role="reader") client = APIClient() client.force_login(user) @@ -275,7 +282,7 @@ def test_api_document_invitations__list__expired_invitations_still_listed(settin settings.INVITATION_VALIDITY_DURATION = 1 # second expired_invitation = factories.InvitationFactory( document=document, - role="member", + role="reader", issuer=user, ) time.sleep(1) @@ -467,17 +474,20 @@ def test_api_document_invitations__delete__privileged_members( assert response.status_code == status.HTTP_204_NO_CONTENT +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_document_invitations__delete__members(via, mock_user_get_teams): - """Member should not be able to cancel invitation.""" +def test_api_document_invitations_delete_readers_or_editors( + via, role, mock_user_get_teams +): + """Readers or editors should not be able to cancel invitation.""" user = factories.UserFactory() document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role=role ) invitation = factories.InvitationFactory(document=document) diff --git a/src/backend/core/tests/test_api_document_versions.py b/src/backend/core/tests/test_api_document_versions.py index 12d9b61b..c5580eb6 100644 --- a/src/backend/core/tests/test_api_document_versions.py +++ b/src/backend/core/tests/test_api_document_versions.py @@ -437,11 +437,12 @@ def test_api_document_versions_delete_authenticated_private(): assert response.json() == {"detail": "Not found."} +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_document_versions_delete_member(via, mock_user_get_teams): +def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_get_teams): """ Authenticated users should not be allowed to delete a document version for a - document in which they are a simple member. + document in which they are a simple reader or editor. """ user = factories.UserFactory() @@ -450,11 +451,11 @@ def test_api_document_versions_delete_member(via, mock_user_get_teams): document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role=role ) # Create a new version should make it available to the user diff --git a/src/backend/core/tests/test_api_template_accesses.py b/src/backend/core/tests/test_api_template_accesses.py index fb468173..dddd96cf 100644 --- a/src/backend/core/tests/test_api_template_accesses.py +++ b/src/backend/core/tests/test_api_template_accesses.py @@ -254,9 +254,12 @@ def test_api_template_accesses_create_authenticated_unrelated(): assert not models.TemplateAccess.objects.filter(user=other_user).exists() +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_create_authenticated_member(via, mock_user_get_teams): - """Members of a template should not be allowed to create template accesses.""" +def test_api_template_accesses_create_authenticated_editor_or_reader( + via, role, mock_user_get_teams +): + """Editors or readers of a template should not be allowed to create template accesses.""" user = factories.UserFactory() client = APIClient() @@ -264,21 +267,21 @@ def test_api_template_accesses_create_authenticated_member(via, mock_user_get_te template = factories.TemplateFactory() if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="member") + factories.UserTemplateAccessFactory(template=template, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="member" + template=template, team="lasuite", role=role ) other_user = factories.UserFactory() - for role in [role[0] for role in models.RoleChoices.choices]: + for new_role in [role[0] for role in models.RoleChoices.choices]: response = client.post( f"/api/v1.0/templates/{template.id!s}/accesses/", { "user": str(other_user.id), - "role": role, + "role": new_role, }, format="json", ) @@ -456,9 +459,12 @@ def test_api_template_accesses_update_authenticated_unrelated(): assert updated_values == old_values +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_authenticated_member(via, mock_user_get_teams): - """Members of a template should not be allowed to update its accesses.""" +def test_api_template_accesses_update_authenticated_editor_or_reader( + via, role, mock_user_get_teams +): + """Editors or readers of a template should not be allowed to update its accesses.""" user = factories.UserFactory() client = APIClient() @@ -466,11 +472,11 @@ def test_api_template_accesses_update_authenticated_member(via, mock_user_get_te template = factories.TemplateFactory() if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="member") + factories.UserTemplateAccessFactory(template=template, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="member" + template=template, team="lasuite", role=role ) access = factories.UserTemplateAccessFactory(template=template) @@ -521,14 +527,14 @@ def test_api_template_accesses_update_administrator_except_owner( access = factories.UserTemplateAccessFactory( template=template, - role=random.choice(["administrator", "member"]), + role=random.choice(["administrator", "editor", "reader"]), ) old_values = serializers.TemplateAccessSerializer(instance=access).data new_values = { "id": uuid4(), "user_id": factories.UserFactory().id, - "role": random.choice(["administrator", "member"]), + "role": random.choice(["administrator", "editor", "reader"]), } for field, value in new_values.items(): @@ -629,7 +635,7 @@ def test_api_template_accesses_update_administrator_to_owner(via, mock_user_get_ access = factories.UserTemplateAccessFactory( template=template, user=other_user, - role=random.choice(["administrator", "member"]), + role=random.choice(["administrator", "editor", "reader"]), ) old_values = serializers.TemplateAccessSerializer(instance=access).data @@ -736,7 +742,7 @@ def test_api_template_accesses_update_owner_self(via, mock_user_get_teams): ) old_values = serializers.TemplateAccessSerializer(instance=access).data - new_role = random.choice(["administrator", "member"]) + new_role = random.choice(["administrator", "editor", "reader"]) response = client.put( f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", @@ -797,11 +803,12 @@ def test_api_template_accesses_delete_authenticated(): assert models.TemplateAccess.objects.count() == 1 +@pytest.mark.parametrize("role", ["reader", "editor"]) @pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_delete_member(via, mock_user_get_teams): +def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_get_teams): """ Authenticated users should not be allowed to delete a template access for a - template in which they are a simple member. + template in which they are a simple editor or reader. """ user = factories.UserFactory() @@ -810,11 +817,11 @@ def test_api_template_accesses_delete_member(via, mock_user_get_teams): template = factories.TemplateFactory() if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="member") + factories.UserTemplateAccessFactory(template=template, user=user, role=role) elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="member" + template=template, team="lasuite", role=role ) access = factories.UserTemplateAccessFactory(template=template) @@ -855,7 +862,7 @@ def test_api_template_accesses_delete_administrators_except_owners( ) access = factories.UserTemplateAccessFactory( - template=template, role=random.choice(["member", "administrator"]) + template=template, role=random.choice(["reader", "editor", "administrator"]) ) assert models.TemplateAccess.objects.count() == 2 diff --git a/src/backend/core/tests/test_models_document_accesses.py b/src/backend/core/tests/test_models_document_accesses.py index e5d5299e..8d5a3dec 100644 --- a/src/backend/core/tests/test_models_document_accesses.py +++ b/src/backend/core/tests/test_models_document_accesses.py @@ -17,11 +17,11 @@ def test_models_document_accesses_str(): """ user = factories.UserFactory(email="david.bowman@example.com") access = factories.UserDocumentAccessFactory( - role="member", + role="reader", user=user, document__title="admins", ) - assert str(access) == "david.bowman@example.com is member in document admins" + assert str(access) == "david.bowman@example.com is reader in document admins" def test_models_document_accesses_unique_user(): @@ -119,7 +119,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["administrator", "member"], + "set_role_to": ["administrator", "editor", "reader"], } @@ -149,7 +149,7 @@ def test_models_document_access_get_abilities_for_owner_of_owner(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["administrator", "member"], + "set_role_to": ["administrator", "editor", "reader"], } @@ -165,13 +165,13 @@ def test_models_document_access_get_abilities_for_owner_of_administrator(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["owner", "member"], + "set_role_to": ["owner", "editor", "reader"], } -def test_models_document_access_get_abilities_for_owner_of_member(): - """Check abilities of member access for the owner of a document.""" - access = factories.UserDocumentAccessFactory(role="member") +def test_models_document_access_get_abilities_for_owner_of_editor(): + """Check abilities of editor access for the owner of a document.""" + access = factories.UserDocumentAccessFactory(role="editor") factories.UserDocumentAccessFactory(document=access.document) # another one user = factories.UserDocumentAccessFactory( document=access.document, role="owner" @@ -181,7 +181,23 @@ def test_models_document_access_get_abilities_for_owner_of_member(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["owner", "administrator"], + "set_role_to": ["owner", "administrator", "reader"], + } + + +def test_models_document_access_get_abilities_for_owner_of_reader(): + """Check abilities of reader access for the owner of a document.""" + access = factories.UserDocumentAccessFactory(role="reader") + factories.UserDocumentAccessFactory(document=access.document) # another one + user = factories.UserDocumentAccessFactory( + document=access.document, role="owner" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": True, + "retrieve": True, + "update": True, + "set_role_to": ["owner", "administrator", "editor"], } @@ -216,13 +232,13 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["member"], + "set_role_to": ["editor", "reader"], } -def test_models_document_access_get_abilities_for_administrator_of_member(): - """Check abilities of member access for the administrator of a document.""" - access = factories.UserDocumentAccessFactory(role="member") +def test_models_document_access_get_abilities_for_administrator_of_editor(): + """Check abilities of editor access for the administrator of a document.""" + access = factories.UserDocumentAccessFactory(role="editor") factories.UserDocumentAccessFactory(document=access.document) # another one user = factories.UserDocumentAccessFactory( document=access.document, role="administrator" @@ -232,19 +248,35 @@ def test_models_document_access_get_abilities_for_administrator_of_member(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["administrator"], + "set_role_to": ["administrator", "reader"], } -# - for member +def test_models_document_access_get_abilities_for_administrator_of_reader(): + """Check abilities of reader access for the administrator of a document.""" + access = factories.UserDocumentAccessFactory(role="reader") + factories.UserDocumentAccessFactory(document=access.document) # another one + user = factories.UserDocumentAccessFactory( + document=access.document, role="administrator" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": True, + "retrieve": True, + "update": True, + "set_role_to": ["administrator", "editor"], + } -def test_models_document_access_get_abilities_for_member_of_owner(): - """Check abilities of owner access for the member of a document.""" +# - for editor + + +def test_models_document_access_get_abilities_for_editor_of_owner(): + """Check abilities of owner access for the editor of a document.""" access = factories.UserDocumentAccessFactory(role="owner") factories.UserDocumentAccessFactory(document=access.document) # another one user = factories.UserDocumentAccessFactory( - document=access.document, role="member" + document=access.document, role="editor" ).user abilities = access.get_abilities(user) assert abilities == { @@ -255,12 +287,12 @@ def test_models_document_access_get_abilities_for_member_of_owner(): } -def test_models_document_access_get_abilities_for_member_of_administrator(): - """Check abilities of administrator access for the member of a document.""" +def test_models_document_access_get_abilities_for_editor_of_administrator(): + """Check abilities of administrator access for the editor of a document.""" access = factories.UserDocumentAccessFactory(role="administrator") factories.UserDocumentAccessFactory(document=access.document) # another one user = factories.UserDocumentAccessFactory( - document=access.document, role="member" + document=access.document, role="editor" ).user abilities = access.get_abilities(user) assert abilities == { @@ -271,14 +303,70 @@ def test_models_document_access_get_abilities_for_member_of_administrator(): } -def test_models_document_access_get_abilities_for_member_of_member_user( +def test_models_document_access_get_abilities_for_editor_of_editor_user( django_assert_num_queries ): - """Check abilities of member access for the member of a document.""" - access = factories.UserDocumentAccessFactory(role="member") + """Check abilities of editor access for the editor of a document.""" + access = factories.UserDocumentAccessFactory(role="editor") factories.UserDocumentAccessFactory(document=access.document) # another one user = factories.UserDocumentAccessFactory( - document=access.document, role="member" + document=access.document, role="editor" + ).user + + with django_assert_num_queries(1): + abilities = access.get_abilities(user) + + assert abilities == { + "destroy": False, + "retrieve": True, + "update": False, + "set_role_to": [], + } + + +# - for reader + + +def test_models_document_access_get_abilities_for_reader_of_owner(): + """Check abilities of owner access for the reader of a document.""" + access = factories.UserDocumentAccessFactory(role="owner") + factories.UserDocumentAccessFactory(document=access.document) # another one + user = factories.UserDocumentAccessFactory( + document=access.document, role="reader" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": False, + "retrieve": True, + "update": False, + "set_role_to": [], + } + + +def test_models_document_access_get_abilities_for_reader_of_administrator(): + """Check abilities of administrator access for the reader of a document.""" + access = factories.UserDocumentAccessFactory(role="administrator") + factories.UserDocumentAccessFactory(document=access.document) # another one + user = factories.UserDocumentAccessFactory( + document=access.document, role="reader" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": False, + "retrieve": True, + "update": False, + "set_role_to": [], + } + + +def test_models_document_access_get_abilities_for_reader_of_reader_user( + django_assert_num_queries +): + """Check abilities of reader access for the reader of a document.""" + access = factories.UserDocumentAccessFactory(role="reader") + factories.UserDocumentAccessFactory(document=access.document) # another one + user = factories.UserDocumentAccessFactory( + document=access.document, role="reader" ).user with django_assert_num_queries(1): @@ -294,11 +382,11 @@ def test_models_document_access_get_abilities_for_member_of_member_user( def test_models_document_access_get_abilities_preset_role(django_assert_num_queries): """No query is done if the role is preset, e.g., with a query annotation.""" - access = factories.UserDocumentAccessFactory(role="member") + access = factories.UserDocumentAccessFactory(role="reader") user = factories.UserDocumentAccessFactory( - document=access.document, role="member" + document=access.document, role="reader" ).user - access.user_roles = ["member"] + access.user_roles = ["reader"] with django_assert_num_queries(0): abilities = access.get_abilities(user) diff --git a/src/backend/core/tests/test_models_documents.py b/src/backend/core/tests/test_models_documents.py index b747dd79..c4c58901 100644 --- a/src/backend/core/tests/test_models_documents.py +++ b/src/backend/core/tests/test_models_documents.py @@ -147,9 +147,28 @@ def test_models_documents_get_abilities_administrator(): } -def test_models_documents_get_abilities_member_user(django_assert_num_queries): - """Check abilities returned for the member of a document.""" - access = factories.UserDocumentAccessFactory(role="member") +def test_models_documents_get_abilities_editor_user(django_assert_num_queries): + """Check abilities returned for the editor of a document.""" + access = factories.UserDocumentAccessFactory(role="editor") + + with django_assert_num_queries(1): + abilities = access.document.get_abilities(access.user) + + assert abilities == { + "destroy": False, + "retrieve": True, + "update": True, + "manage_accesses": False, + "partial_update": True, + "versions_destroy": False, + "versions_list": True, + "versions_retrieve": True, + } + + +def test_models_documents_get_abilities_reader_user(django_assert_num_queries): + """Check abilities returned for the reader of a document.""" + access = factories.UserDocumentAccessFactory(role="reader") with django_assert_num_queries(1): abilities = access.document.get_abilities(access.user) @@ -168,8 +187,8 @@ def test_models_documents_get_abilities_member_user(django_assert_num_queries): def test_models_documents_get_abilities_preset_role(django_assert_num_queries): """No query is done if the role is preset e.g. with query annotation.""" - access = factories.UserDocumentAccessFactory(role="member") - access.document.user_roles = ["member"] + access = factories.UserDocumentAccessFactory(role="reader") + access.document.user_roles = ["reader"] with django_assert_num_queries(0): abilities = access.document.get_abilities(access.user) diff --git a/src/backend/core/tests/test_models_invitations.py b/src/backend/core/tests/test_models_invitations.py index 39dea6d3..d07b5818 100644 --- a/src/backend/core/tests/test_models_invitations.py +++ b/src/backend/core/tests/test_models_invitations.py @@ -170,7 +170,7 @@ def test_models_invitation__new_user__user_creation_constant_num_queries( def test_models_document_invitations_email(): """Check email invitation during invitation creation.""" - member_access = factories.UserDocumentAccessFactory(role="member") + member_access = factories.UserDocumentAccessFactory(role="reader") document = member_access.document # pylint: disable-next=no-member @@ -201,7 +201,7 @@ def test_models_document_invitations_email(): def test_models_document_invitations_email_failed(mock_logger, _mock_send_mail): """Check invitation behavior when an SMTP error occurs during invitation creation.""" - member_access = factories.UserDocumentAccessFactory(role="member") + member_access = factories.UserDocumentAccessFactory(role="reader") document = member_access.document # pylint: disable-next=no-member @@ -288,17 +288,42 @@ def test_models_document_invitations_get_abilities_privileged_member( @pytest.mark.parametrize("via", VIA) -def test_models_document_invitations_get_abilities_member(via, mock_user_get_teams): - """Check abilities for a document member with 'member' role.""" +def test_models_document_invitations_get_abilities_reader(via, mock_user_get_teams): + """Check abilities for a document reader with 'reader' role.""" user = factories.UserFactory() document = factories.DocumentFactory() if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role="member") + factories.UserDocumentAccessFactory(document=document, user=user, role="reader") elif via == TEAM: mock_user_get_teams.return_value = ["lasuite", "unknown"] factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role="member" + document=document, team="lasuite", role="reader" + ) + + invitation = factories.InvitationFactory(document=document) + abilities = invitation.get_abilities(user) + + assert abilities == { + "destroy": False, + "retrieve": True, + "partial_update": False, + "update": False, + } + + +@pytest.mark.parametrize("via", VIA) +def test_models_document_invitations_get_abilities_editor(via, mock_user_get_teams): + """Check abilities for a document editor with 'editor' role.""" + + user = factories.UserFactory() + document = factories.DocumentFactory() + if via == USER: + factories.UserDocumentAccessFactory(document=document, user=user, role="editor") + elif via == TEAM: + mock_user_get_teams.return_value = ["lasuite", "unknown"] + factories.TeamDocumentAccessFactory( + document=document, team="lasuite", role="editor" ) invitation = factories.InvitationFactory(document=document) diff --git a/src/backend/core/tests/test_models_template_accesses.py b/src/backend/core/tests/test_models_template_accesses.py index c3816c5a..b15a19bb 100644 --- a/src/backend/core/tests/test_models_template_accesses.py +++ b/src/backend/core/tests/test_models_template_accesses.py @@ -17,11 +17,11 @@ def test_models_template_accesses_str(): """ user = factories.UserFactory(email="david.bowman@example.com") access = factories.UserTemplateAccessFactory( - role="member", + role="reader", user=user, template__title="admins", ) - assert str(access) == "david.bowman@example.com is member in template admins" + assert str(access) == "david.bowman@example.com is reader in template admins" def test_models_template_accesses_unique_user(): @@ -119,7 +119,7 @@ def test_models_template_access_get_abilities_for_owner_of_self_allowed(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["administrator", "member"], + "set_role_to": ["administrator", "editor", "reader"], } @@ -149,7 +149,7 @@ def test_models_template_access_get_abilities_for_owner_of_owner(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["administrator", "member"], + "set_role_to": ["administrator", "editor", "reader"], } @@ -165,13 +165,13 @@ def test_models_template_access_get_abilities_for_owner_of_administrator(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["owner", "member"], + "set_role_to": ["owner", "editor", "reader"], } -def test_models_template_access_get_abilities_for_owner_of_member(): - """Check abilities of member access for the owner of a template.""" - access = factories.UserTemplateAccessFactory(role="member") +def test_models_template_access_get_abilities_for_owner_of_editor(): + """Check abilities of editor access for the owner of a template.""" + access = factories.UserTemplateAccessFactory(role="editor") factories.UserTemplateAccessFactory(template=access.template) # another one user = factories.UserTemplateAccessFactory( template=access.template, role="owner" @@ -181,7 +181,23 @@ def test_models_template_access_get_abilities_for_owner_of_member(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["owner", "administrator"], + "set_role_to": ["owner", "administrator", "reader"], + } + + +def test_models_template_access_get_abilities_for_owner_of_reader(): + """Check abilities of reader access for the owner of a template.""" + access = factories.UserTemplateAccessFactory(role="reader") + factories.UserTemplateAccessFactory(template=access.template) # another one + user = factories.UserTemplateAccessFactory( + template=access.template, role="owner" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": True, + "retrieve": True, + "update": True, + "set_role_to": ["owner", "administrator", "editor"], } @@ -216,13 +232,13 @@ def test_models_template_access_get_abilities_for_administrator_of_administrator "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["member"], + "set_role_to": ["editor", "reader"], } -def test_models_template_access_get_abilities_for_administrator_of_member(): - """Check abilities of member access for the administrator of a template.""" - access = factories.UserTemplateAccessFactory(role="member") +def test_models_template_access_get_abilities_for_administrator_of_editor(): + """Check abilities of editor access for the administrator of a template.""" + access = factories.UserTemplateAccessFactory(role="editor") factories.UserTemplateAccessFactory(template=access.template) # another one user = factories.UserTemplateAccessFactory( template=access.template, role="administrator" @@ -232,19 +248,35 @@ def test_models_template_access_get_abilities_for_administrator_of_member(): "destroy": True, "retrieve": True, "update": True, - "set_role_to": ["administrator"], + "set_role_to": ["administrator", "reader"], } -# - for member +def test_models_template_access_get_abilities_for_administrator_of_reader(): + """Check abilities of reader access for the administrator of a template.""" + access = factories.UserTemplateAccessFactory(role="reader") + factories.UserTemplateAccessFactory(template=access.template) # another one + user = factories.UserTemplateAccessFactory( + template=access.template, role="administrator" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": True, + "retrieve": True, + "update": True, + "set_role_to": ["administrator", "editor"], + } -def test_models_template_access_get_abilities_for_member_of_owner(): - """Check abilities of owner access for the member of a template.""" +# - For editor + + +def test_models_template_access_get_abilities_for_editor_of_owner(): + """Check abilities of owner access for the editor of a template.""" access = factories.UserTemplateAccessFactory(role="owner") factories.UserTemplateAccessFactory(template=access.template) # another one user = factories.UserTemplateAccessFactory( - template=access.template, role="member" + template=access.template, role="editor" ).user abilities = access.get_abilities(user) assert abilities == { @@ -255,12 +287,12 @@ def test_models_template_access_get_abilities_for_member_of_owner(): } -def test_models_template_access_get_abilities_for_member_of_administrator(): - """Check abilities of administrator access for the member of a template.""" +def test_models_template_access_get_abilities_for_editor_of_administrator(): + """Check abilities of administrator access for the editor of a template.""" access = factories.UserTemplateAccessFactory(role="administrator") factories.UserTemplateAccessFactory(template=access.template) # another one user = factories.UserTemplateAccessFactory( - template=access.template, role="member" + template=access.template, role="editor" ).user abilities = access.get_abilities(user) assert abilities == { @@ -271,14 +303,70 @@ def test_models_template_access_get_abilities_for_member_of_administrator(): } -def test_models_template_access_get_abilities_for_member_of_member_user( +def test_models_template_access_get_abilities_for_editor_of_editor_user( django_assert_num_queries ): - """Check abilities of member access for the member of a template.""" - access = factories.UserTemplateAccessFactory(role="member") + """Check abilities of editor access for the editor of a template.""" + access = factories.UserTemplateAccessFactory(role="editor") factories.UserTemplateAccessFactory(template=access.template) # another one user = factories.UserTemplateAccessFactory( - template=access.template, role="member" + template=access.template, role="editor" + ).user + + with django_assert_num_queries(1): + abilities = access.get_abilities(user) + + assert abilities == { + "destroy": False, + "retrieve": True, + "update": False, + "set_role_to": [], + } + + +# - For reader + + +def test_models_template_access_get_abilities_for_reader_of_owner(): + """Check abilities of owner access for the reader of a template.""" + access = factories.UserTemplateAccessFactory(role="owner") + factories.UserTemplateAccessFactory(template=access.template) # another one + user = factories.UserTemplateAccessFactory( + template=access.template, role="reader" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": False, + "retrieve": True, + "update": False, + "set_role_to": [], + } + + +def test_models_template_access_get_abilities_for_reader_of_administrator(): + """Check abilities of administrator access for the reader of a template.""" + access = factories.UserTemplateAccessFactory(role="administrator") + factories.UserTemplateAccessFactory(template=access.template) # another one + user = factories.UserTemplateAccessFactory( + template=access.template, role="reader" + ).user + abilities = access.get_abilities(user) + assert abilities == { + "destroy": False, + "retrieve": True, + "update": False, + "set_role_to": [], + } + + +def test_models_template_access_get_abilities_for_reader_of_reader_user( + django_assert_num_queries +): + """Check abilities of reader access for the reader of a template.""" + access = factories.UserTemplateAccessFactory(role="reader") + factories.UserTemplateAccessFactory(template=access.template) # another one + user = factories.UserTemplateAccessFactory( + template=access.template, role="reader" ).user with django_assert_num_queries(1): @@ -294,11 +382,11 @@ def test_models_template_access_get_abilities_for_member_of_member_user( def test_models_template_access_get_abilities_preset_role(django_assert_num_queries): """No query is done if the role is preset, e.g., with a query annotation.""" - access = factories.UserTemplateAccessFactory(role="member") + access = factories.UserTemplateAccessFactory(role="reader") user = factories.UserTemplateAccessFactory( - template=access.template, role="member" + template=access.template, role="reader" ).user - access.user_roles = ["member"] + access.user_roles = ["reader"] with django_assert_num_queries(0): abilities = access.get_abilities(user) diff --git a/src/backend/core/tests/test_models_templates.py b/src/backend/core/tests/test_models_templates.py index 79304e0f..bd2269e8 100644 --- a/src/backend/core/tests/test_models_templates.py +++ b/src/backend/core/tests/test_models_templates.py @@ -134,9 +134,26 @@ def test_models_templates_get_abilities_administrator(): } -def test_models_templates_get_abilities_member_user(django_assert_num_queries): - """Check abilities returned for the member of a template.""" - access = factories.UserTemplateAccessFactory(role="member") +def test_models_templates_get_abilities_editor_user(django_assert_num_queries): + """Check abilities returned for the editor of a template.""" + access = factories.UserTemplateAccessFactory(role="editor") + + with django_assert_num_queries(1): + abilities = access.template.get_abilities(access.user) + + assert abilities == { + "destroy": False, + "retrieve": True, + "update": True, + "manage_accesses": False, + "partial_update": True, + "generate_document": True, + } + + +def test_models_templates_get_abilities_reader_user(django_assert_num_queries): + """Check abilities returned for the reader of a template.""" + access = factories.UserTemplateAccessFactory(role="reader") with django_assert_num_queries(1): abilities = access.template.get_abilities(access.user) @@ -153,8 +170,8 @@ def test_models_templates_get_abilities_member_user(django_assert_num_queries): def test_models_templates_get_abilities_preset_role(django_assert_num_queries): """No query is done if the role is preset e.g. with query annotation.""" - access = factories.UserTemplateAccessFactory(role="member") - access.template.user_roles = ["member"] + access = factories.UserTemplateAccessFactory(role="reader") + access.template.user_roles = ["reader"] with django_assert_num_queries(0): abilities = access.template.get_abilities(access.user) diff --git a/src/frontend/apps/impress/src/features/pads/pad-management/types.tsx b/src/frontend/apps/impress/src/features/pads/pad-management/types.tsx index 808cbff1..733d3527 100644 --- a/src/frontend/apps/impress/src/features/pads/pad-management/types.tsx +++ b/src/frontend/apps/impress/src/features/pads/pad-management/types.tsx @@ -12,7 +12,8 @@ export interface Access { } export enum Role { - MEMBER = 'member', + READER = 'reader', + EDITOR = 'editor', ADMIN = 'administrator', OWNER = 'owner', } diff --git a/src/frontend/apps/impress/src/features/pads/pad-tools/types.ts b/src/frontend/apps/impress/src/features/pads/pad-tools/types.ts index 8f395b9d..bac7d27e 100644 --- a/src/frontend/apps/impress/src/features/pads/pad-tools/types.ts +++ b/src/frontend/apps/impress/src/features/pads/pad-tools/types.ts @@ -1,5 +1,6 @@ export enum Role { - MEMBER = 'member', + READER = 'reader', + EDITOR = 'editor', ADMIN = 'administrator', OWNER = 'owner', }