Compare commits

...

10 Commits

Author SHA1 Message Date
Anthony LC
3eff663a78 ♻️(frontend) adapt doc visibility to new api
We updated the way we handle the visibility of a doc
in the backend. Now we use a new api to update
the visibility (documents/{id}/link-configuration/)
of a doc. We adapted the frontend to use this new api.
We changed the types to reflect the new api and
to keep the same logic.
2024-09-11 10:23:06 +02:00
Anthony LC
feabab030d fixup! (models/api) add link access reach and role 2024-09-10 16:07:43 +02:00
Samuel Paccoud - DINUM
4002a049eb (models) allow null titles on documents
We want to make it as fast as possible to create a new document.
We should not have any modal asking the title before creating the
document but rather show an "untitle document" title and let the
owner set it on the already created document.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM
c7d8211aac (api) allow forcing ID when creating a document via API endpoint
We need to be able to force the ID when creating a document via
the API endpoint. This is usefull for documents that are created
offline as synchronization is achieved by replaying stacked requests.

We do it via the serializer, making sure that we don't override an
existing document.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM
25a24eaf12 🔥(api) remove possibility to force document id on creation
This feature poses security issues in the way it is implemented.
We decide to remove it while clarifying the use case.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM
9adecf93ec (api) allow updating link configuration for a document
We open a specific endpoint to update documents link configuration
because it makes it more secure and simple to limit access rights
to administrators/owners whereas other document fields like title
and content can be edited by anonymous or authenticated users with
much less access rights.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM
6a3a07db31 🐛(api) fix randomly failing test on document list ordering via API
The test was randomly failing because postgresql and python sorting
was not 100% consistent e.g "treatment" vs "treat them" were not
ordered the same.

Comparing each field value insteat of relying on "sort" solves the
issue and makes the test simpler.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM
79b56ccd4e (models/api) add link access reach and role
Link access was either public or private and was only allowing readers.

This commit makes link access more powerful:
- link reach can be private (users need to obtain specific access by
  document's administrators), restricted (any authenticated user) or
  public (anybody including anonymous users)
- link role can be reader or editor.

It is thus now possible to give editor access to an anonymous user or
any authenticated user.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM
611d77f3bb 🔥(compose) remove docker compose version
The version is now automatically guessed by Docker Compose. This
commit will fix the warning raised about the uselessness of this
setting.
2024-09-10 16:02:25 +02:00
Anthony LC
de0acedc0a 🛂(backend) stop to list public doc to everyone
Everybody could see the full list of public docs.
Now only members can see their public docs.
They can still access to any specific public doc.
2024-09-10 16:02:25 +02:00
51 changed files with 1375 additions and 741 deletions

View File

@@ -32,6 +32,7 @@ and this project adheres to
- 💄(frontend) code background darkened on editor #214
- 🔥(frontend) hide markdown button if not text #213
- 🛂(backend) stop to list public doc to everyone #234
## Fixed

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
postgresql:
image: postgres:16

View File

@@ -92,6 +92,14 @@ class DocumentAdmin(admin.ModelAdmin):
"""Document admin interface declaration."""
inlines = (DocumentAccessInline,)
list_display = (
"id",
"title",
"link_reach",
"link_role",
"created_at",
"updated_at",
)
@admin.register(models.Invitation)

View File

@@ -62,6 +62,9 @@ class IsOwnedOrPublic(IsAuthenticated):
class AccessPermission(permissions.BasePermission):
"""Permission class for access objects."""
def has_permission(self, request, view):
return request.user.is_authenticated or view.action != "create"
def has_object_permission(self, request, view, obj):
"""Check permission for a given object."""
abilities = obj.get_abilities(request.user)

View File

@@ -66,9 +66,8 @@ class BaseAccessSerializer(serializers.ModelSerializer):
"You must set a resource ID in kwargs to create a new access."
) from exc
teams = user.get_teams()
if not self.Meta.model.objects.filter( # pylint: disable=no-member
Q(user=user) | Q(team__in=teams),
Q(user=user) | Q(team__in=user.teams),
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
).exists():
raise exceptions.PermissionDenied(
@@ -78,7 +77,7 @@ class BaseAccessSerializer(serializers.ModelSerializer):
if (
role == models.RoleChoices.OWNER
and not self.Meta.model.objects.filter( # pylint: disable=no-member
Q(user=user) | Q(team__in=teams),
Q(user=user) | Q(team__in=user.teams),
role=models.RoleChoices.OWNER,
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
).exists()
@@ -149,11 +148,57 @@ class DocumentSerializer(BaseResourceSerializer):
"title",
"accesses",
"abilities",
"is_public",
"link_role",
"link_reach",
"created_at",
"updated_at",
]
read_only_fields = ["id", "accesses", "abilities", "created_at", "updated_at"]
read_only_fields = [
"id",
"accesses",
"abilities",
"link_role",
"link_reach",
"created_at",
"updated_at",
]
def get_fields(self):
"""Dynamically make `id` read-only on PUT requests but writable on POST requests."""
fields = super().get_fields()
request = self.context.get("request")
if request and request.method == "POST":
fields["id"].read_only = False
return fields
def validate_id(self, value):
"""Ensure the provided ID does not already exist when creating a new document."""
request = self.context.get("request")
# Only check this on POST (creation)
if request and request.method == "POST":
if models.Document.objects.filter(id=value).exists():
raise serializers.ValidationError(
"A document with this ID already exists. You cannot override it."
)
return value
class LinkDocumentSerializer(BaseResourceSerializer):
"""
Serialize link configuration for documents.
We expose it separately from document in order to simplify and secure access control.
"""
class Meta:
model = models.Document
fields = [
"link_role",
"link_reach",
]
# Suppress the warning about not implementing `create` and `update` methods
@@ -272,9 +317,8 @@ class InvitationSerializer(serializers.ModelSerializer):
"Anonymous users are not allowed to create invitations."
)
teams = user.get_teams()
if not models.DocumentAccess.objects.filter(
Q(user=user) | Q(team__in=teams),
Q(user=user) | Q(team__in=user.teams),
document=document_id,
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
).exists():
@@ -285,7 +329,7 @@ class InvitationSerializer(serializers.ModelSerializer):
if (
role == models.RoleChoices.OWNER
and not models.DocumentAccess.objects.filter(
Q(user=user) | Q(team__in=teams),
Q(user=user) | Q(team__in=user.teams),
document=document_id,
role=models.RoleChoices.OWNER,
).exists()

View File

@@ -8,6 +8,7 @@ from urllib.parse import urlparse
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.core.files.storage import default_storage
from django.db import IntegrityError
from django.db.models import (
OuterRef,
Q,
@@ -185,28 +186,21 @@ class ResourceViewsetMixin:
def get_queryset(self):
"""Custom queryset to get user related resources."""
queryset = super().get_queryset()
if not self.request.user.is_authenticated:
return queryset.filter(is_public=True)
user = self.request.user
teams = user.get_teams()
if not user.is_authenticated:
return queryset
user_roles_query = (
self.access_model_class.objects.filter(
Q(user=user) | Q(team__in=teams),
Q(user=user) | Q(team__in=user.teams),
**{self.resource_field_name: OuterRef("pk")},
)
.values(self.resource_field_name)
.annotate(roles_array=ArrayAgg("role"))
.values("roles_array")
)
return (
queryset.filter(
Q(accesses__user=user) | Q(accesses__team__in=teams) | Q(is_public=True)
)
.annotate(user_roles=Subquery(user_roles_query))
.distinct()
)
return queryset.annotate(user_roles=Subquery(user_roles_query)).distinct()
def perform_create(self, serializer):
"""Set the current user as owner of the newly created object."""
@@ -245,8 +239,7 @@ class ResourceAccessViewsetMixin:
if self.action == "list":
user = self.request.user
teams = user.get_teams()
teams = user.teams
user_roles_query = (
queryset.filter(
Q(user=user) | Q(team__in=teams),
@@ -314,15 +307,12 @@ class DocumentViewSet(
ResourceViewsetMixin,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet,
):
"""Document ViewSet"""
permission_classes = [
permissions.IsAuthenticatedOrSafe,
permissions.AccessPermission,
]
serializer_class = serializers.DocumentSerializer
@@ -331,18 +321,52 @@ class DocumentViewSet(
queryset = models.Document.objects.all()
ordering = ["-updated_at"]
def perform_create(self, serializer):
"""
Override perform_create to use the provided ID in the payload if it exists
"""
document_id = self.request.data.get("id")
document = serializer.save(id=document_id) if document_id else serializer.save()
def list(self, request, *args, **kwargs):
"""Restrict resources returned by the list endpoint"""
queryset = self.filter_queryset(self.get_queryset())
user = self.request.user
if user.is_authenticated:
queryset = queryset.filter(
Q(accesses__user=user)
| Q(accesses__team__in=user.teams)
| (
Q(link_traces__user=user)
& ~Q(link_reach=models.LinkReachChoices.RESTRICTED)
)
)
else:
queryset = queryset.none()
self.access_model_class.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
**{self.resource_field_name: document},
)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return drf_response.Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
"""
Add a trace that the document was accessed by a user. This is used to list documents
on a user's list view even though the user has no specific role in the document (link
access when the link reach configuration of the document allows it).
"""
instance = self.get_object()
serializer = self.get_serializer(instance)
if self.request.user.is_authenticated:
try:
# Add a trace that the user visited the document (this is needed to include
# the document in the user's list view)
models.LinkTrace.objects.update_or_create(
document=instance,
user=self.request.user,
)
except IntegrityError:
# The trace already exists, so we just pass without doing anything
pass
return drf_response.Response(serializer.data)
@decorators.action(detail=True, methods=["get"], url_path="versions")
def versions_list(self, request, *args, **kwargs):
@@ -350,11 +374,15 @@ class DocumentViewSet(
Return the document's versions but only those created after the user got access
to the document
"""
if not request.user.is_authenticated:
return drf_response.Response([])
document = self.get_object()
user = request.user
from_datetime = min(
access.created_at
for access in document.accesses.filter(
Q(user=request.user) | Q(team__in=request.user.get_teams()),
Q(user=user) | Q(team__in=user.teams),
)
)
@@ -386,10 +414,11 @@ class DocumentViewSet(
# Don't let users access versions that were created before they were given access
# to the document
user = request.user
from_datetime = min(
access.created_at
for access in document.accesses.filter(
Q(user=request.user) | Q(team__in=request.user.get_teams()),
Q(user=user) | Q(team__in=user.teams),
)
)
if response["LastModified"] < from_datetime:
@@ -409,6 +438,24 @@ class DocumentViewSet(
}
)
@decorators.action(detail=True, methods=["put"], url_path="link-configuration")
def link_configuration(self, request, *args, **kwargs):
"""Update link configuration with specific rights (cf get_abilities)."""
# Check permissions first
document = self.get_object()
# Deserialize and validate the data
serializer = serializers.LinkDocumentSerializer(
document, data=request.data, partial=True
)
if not serializer.is_valid():
return drf_response.Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
serializer.save()
return drf_response.Response(serializer.data, status=status.HTTP_200_OK)
@decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
def attachment_upload(self, request, *args, **kwargs):
"""Upload a file related to a given document"""
@@ -529,7 +576,6 @@ class TemplateViewSet(
ResourceViewsetMixin,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet,
@@ -545,6 +591,27 @@ class TemplateViewSet(
resource_field_name = "template"
queryset = models.Template.objects.all()
def list(self, request, *args, **kwargs):
"""Restrict templates returned by the list endpoint"""
queryset = self.filter_queryset(self.get_queryset())
user = self.request.user
if user.is_authenticated:
queryset = queryset.filter(
Q(accesses__user=user)
| Q(accesses__team__in=user.teams)
| Q(is_public=True)
)
else:
queryset = queryset.filter(is_public=True)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return drf_response.Response(serializer.data)
@decorators.action(
detail=True,
methods=["post"],
@@ -671,7 +738,7 @@ class InvitationViewset(
if self.action == "list":
user = self.request.user
teams = user.get_teams()
teams = user.teams
# Determine which role the logged-in user has in the document
user_roles_query = (

View File

@@ -35,8 +35,13 @@ class DocumentFactory(factory.django.DjangoModelFactory):
skip_postgeneration_save = True
title = factory.Sequence(lambda n: f"document{n}")
is_public = factory.Faker("boolean")
content = factory.Sequence(lambda n: f"content{n}")
link_reach = factory.fuzzy.FuzzyChoice(
[a[0] for a in models.LinkReachChoices.choices]
)
link_role = factory.fuzzy.FuzzyChoice(
[r[0] for r in models.LinkRoleChoices.choices]
)
@factory.post_generation
def users(self, create, extracted, **kwargs):
@@ -48,6 +53,13 @@ class DocumentFactory(factory.django.DjangoModelFactory):
else:
UserDocumentAccessFactory(document=self, user=item[0], role=item[1])
@factory.post_generation
def link_traces(self, create, extracted, **kwargs):
"""Add link traces to document from a given list of users."""
if create and extracted:
for item in extracted:
models.LinkTrace.objects.create(document=self, user=item)
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
"""Create fake document user accesses for testing."""

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.1 on 2024-09-08 16:55
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_create_pg_trgm_extension'),
]
operations = [
migrations.AddField(
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='restricted', max_length=20),
),
migrations.AddField(
model_name='document',
name='link_role',
field=models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor')], default='reader', max_length=20),
),
migrations.AlterField(
model_name='document',
name='is_public',
field=models.BooleanField(null=True),
),
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='LinkTrace',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to='core.document')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Document/user link trace',
'verbose_name_plural': 'Document/user link traces',
'db_table': 'impress_link_trace',
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_link_trace_document_user', violation_error_message='A link trace already exists for this document/user.')],
},
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.1 on 2024-09-08 17:04
from django.db import migrations
def migrate_is_public_to_link_reach(apps, schema_editor):
"""
Forward migration: Migrate 'is_public' to 'link_reach'.
If is_public == True, set link_reach to 'public'
"""
Document = apps.get_model('core', 'Document')
Document.objects.filter(is_public=True).update(link_reach='public')
def reverse_migrate_link_reach_to_is_public(apps, schema_editor):
"""
Reverse migration: Migrate 'link_reach' back to 'is_public'.
- If link_reach == 'public', set is_public to True
- Else set is_public to False
"""
Document = apps.get_model('core', 'Document')
Document.objects.filter(link_reach='public').update(is_public=True)
Document.objects.filter(link_reach__in=['restricted', "authenticated"]).update(is_public=False)
class Migration(migrations.Migration):
dependencies = [
('core', '0003_document_link_reach_document_link_role_and_more'),
]
operations = [
migrations.RunPython(
migrate_is_public_to_link_reach,
reverse_migrate_link_reach_to_is_public
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2024-09-09 17:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_migrate_is_public_to_link_reach'),
]
operations = [
migrations.AlterField(
model_name='document',
name='title',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'),
),
]

View File

@@ -21,7 +21,7 @@ from django.http import FileResponse
from django.template.base import Template as DjangoTemplate
from django.template.context import Context
from django.utils import html, timezone
from django.utils.functional import lazy
from django.utils.functional import cached_property, lazy
from django.utils.translation import gettext_lazy as _
import frontmatter
@@ -42,18 +42,24 @@ def get_resource_roles(resource, user):
try:
roles = resource.user_roles or []
except AttributeError:
teams = user.get_teams()
try:
roles = resource.accesses.filter(
models.Q(user=user) | models.Q(team__in=teams),
models.Q(user=user) | models.Q(team__in=user.teams),
).values_list("role", flat=True)
except (models.ObjectDoesNotExist, IndexError):
roles = []
return roles
class LinkRoleChoices(models.TextChoices):
"""Defines the possible roles a link can offer on a document."""
READER = "reader", _("Reader") # Can read
EDITOR = "editor", _("Editor") # Can read and edit
class RoleChoices(models.TextChoices):
"""Defines the possible roles a user can have in a template."""
"""Defines the possible roles a user can have in a resource."""
READER = "reader", _("Reader") # Can read
EDITOR = "editor", _("Editor") # Can read and edit
@@ -61,6 +67,20 @@ class RoleChoices(models.TextChoices):
OWNER = "owner", _("Owner")
class LinkReachChoices(models.TextChoices):
"""Defines types of access for links"""
RESTRICTED = (
"restricted",
_("Restricted"),
) # Only users with a specific access can read/edit the document
AUTHENTICATED = (
"authenticated",
_("Authenticated"),
) # Any authenticated user can access the document
PUBLIC = "public", _("Public") # Even anonymous users can access the document
class BaseModel(models.Model):
"""
Serves as an abstract base model for other models, ensuring that records are validated
@@ -215,7 +235,8 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
raise ValueError("User has no email address.")
mail.send_mail(subject, message, from_email, [self.email], **kwargs)
def get_teams(self):
@cached_property
def teams(self):
"""
Get list of teams in which the user is, as a list of strings.
Must be cached if retrieved remotely.
@@ -247,7 +268,7 @@ class BaseAccess(BaseModel):
"""
roles = []
if user.is_authenticated:
teams = user.get_teams()
teams = user.teams
try:
roles = self.user_roles or []
except AttributeError:
@@ -299,11 +320,14 @@ class BaseAccess(BaseModel):
class Document(BaseModel):
"""Pad document carrying the content."""
title = models.CharField(_("title"), max_length=255)
is_public = models.BooleanField(
_("public"),
default=False,
help_text=_("Whether this document is public for anyone to use."),
title = models.CharField(_("title"), max_length=255, null=True, blank=True)
link_reach = models.CharField(
max_length=20,
choices=LinkReachChoices.choices,
default=LinkReachChoices.RESTRICTED,
)
link_role = models.CharField(
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
)
_content = None
@@ -315,7 +339,7 @@ class Document(BaseModel):
verbose_name_plural = _("Documents")
def __str__(self):
return self.title
return str(self.title) if self.title else str(_("Untitled Document"))
def save(self, *args, **kwargs):
"""Write content to object storage only if _content has changed."""
@@ -466,17 +490,29 @@ class Document(BaseModel):
"""
Compute and return abilities for a given user on the document.
"""
roles = get_resource_roles(self, user)
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)
roles = set(get_resource_roles(self, user))
# Compute version roles before adding link roles because we don't
# want anonymous users to access versions (we wouldn't know from
# which date to allow them anyway)
can_get_versions = bool(roles)
# Add role provided by the document link
if self.link_reach == LinkReachChoices.PUBLIC or (
self.link_reach == LinkReachChoices.AUTHENTICATED and user.is_authenticated
):
roles.add(self.link_role)
is_owner_or_admin = bool(
roles.intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
)
is_editor = bool(RoleChoices.EDITOR in roles)
can_get = bool(roles)
return {
"destroy": RoleChoices.OWNER in roles,
"attachment_upload": is_owner_or_admin or is_editor,
"destroy": RoleChoices.OWNER in roles,
"link_configuration": is_owner_or_admin,
"manage_accesses": is_owner_or_admin,
"partial_update": is_owner_or_admin or is_editor,
"retrieve": can_get,
@@ -487,6 +523,38 @@ class Document(BaseModel):
}
class LinkTrace(BaseModel):
"""
Relation model to trace accesses to a document via a link by a logged-in user.
This is necessary to show the document in the user's list of documents even
though the user does not have a role on the document.
"""
document = models.ForeignKey(
Document,
on_delete=models.CASCADE,
related_name="link_traces",
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="link_traces")
class Meta:
db_table = "impress_link_trace"
verbose_name = _("Document/user link trace")
verbose_name_plural = _("Document/user link traces")
constraints = [
models.UniqueConstraint(
fields=["user", "document"],
name="unique_link_trace_document_user",
violation_error_message=_(
"A link trace already exists for this document/user."
),
),
]
def __str__(self):
return f"{self.user!s} trace on document {self.document!s}"
class DocumentAccess(BaseAccess):
"""Relation model to give access to a document for a user or a team with a role."""
@@ -778,7 +846,7 @@ class Invitation(BaseModel):
roles = []
if user.is_authenticated:
teams = user.get_teams()
teams = user.teams
try:
roles = self.user_roles or []
except AttributeError:

View File

@@ -10,7 +10,9 @@ VIA = [USER, TEAM]
@pytest.fixture
def mock_user_get_teams():
"""Mock for the "get_teams" method on the User model."""
with mock.patch("core.models.User.get_teams") as mock_get_teams:
yield mock_get_teams
def mock_user_teams():
"""Mock for the "teams" property on the User model."""
with mock.patch(
"core.models.User.teams", new_callable=mock.PropertyMock
) as mock_teams:
yield mock_teams

View File

@@ -57,7 +57,7 @@ def test_api_document_accesses_list_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_list_authenticated_related(via, mock_user_get_teams):
def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
"""
Authenticated users should be able to list document accesses for a document
to which they are directly related, whatever their role in the document.
@@ -76,7 +76,7 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_get_tea
role=random.choice(models.RoleChoices.choices)[0],
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
user_access = models.DocumentAccess.objects.create(
document=document,
team="lasuite",
@@ -181,7 +181,7 @@ def test_api_document_accesses_retrieve_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_get_teams):
def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_teams):
"""
A user who is related to a document should be allowed to retrieve the
associated document user accesses.
@@ -195,7 +195,7 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_get
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
access = factories.UserDocumentAccessFactory(document=document)
@@ -276,7 +276,7 @@ def test_api_document_accesses_update_authenticated_unrelated():
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_authenticated_reader_or_editor(
via, role, mock_user_get_teams
via, role, mock_user_teams
):
"""Readers or editors of a document should not be allowed to update its accesses."""
user = factories.UserFactory()
@@ -288,7 +288,7 @@ def test_api_document_accesses_update_authenticated_reader_or_editor(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -316,9 +316,7 @@ def test_api_document_accesses_update_authenticated_reader_or_editor(
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_administrator_except_owner(
via, mock_user_get_teams
):
def test_api_document_accesses_update_administrator_except_owner(via, mock_user_teams):
"""
A user who is a direct administrator in a document should be allowed to update a user
access for this document, as long as they don't try to set the role to owner.
@@ -334,7 +332,7 @@ def test_api_document_accesses_update_administrator_except_owner(
document=document, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="administrator"
)
@@ -375,9 +373,7 @@ def test_api_document_accesses_update_administrator_except_owner(
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_administrator_from_owner(
via, mock_user_get_teams
):
def test_api_document_accesses_update_administrator_from_owner(via, mock_user_teams):
"""
A user who is an administrator in a document, should not be allowed to update
the user access of an "owner" for this document.
@@ -393,7 +389,7 @@ def test_api_document_accesses_update_administrator_from_owner(
document=document, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="administrator"
)
@@ -424,7 +420,7 @@ def test_api_document_accesses_update_administrator_from_owner(
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_administrator_to_owner(via, mock_user_get_teams):
def test_api_document_accesses_update_administrator_to_owner(via, mock_user_teams):
"""
A user who is an administrator in a document, should not be allowed to update
the user access of another user to grant document ownership.
@@ -440,7 +436,7 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_get_
document=document, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="administrator"
)
@@ -478,7 +474,7 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_get_
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_owner(via, mock_user_get_teams):
def test_api_document_accesses_update_owner(via, mock_user_teams):
"""
A user who is an owner in a document should be allowed to update
a user access for this document whatever the role.
@@ -492,7 +488,7 @@ def test_api_document_accesses_update_owner(via, mock_user_get_teams):
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
@@ -534,7 +530,7 @@ def test_api_document_accesses_update_owner(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_owner_self(via, mock_user_get_teams):
def test_api_document_accesses_update_owner_self(via, mock_user_teams):
"""
A user who is owner of a document should be allowed to update
their own user access provided there are other owners in the document.
@@ -551,7 +547,7 @@ def test_api_document_accesses_update_owner_self(via, mock_user_get_teams):
document=document, user=user, role="owner"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
@@ -626,7 +622,7 @@ def test_api_document_accesses_delete_authenticated():
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_get_teams):
def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_teams):
"""
Authenticated users should not be allowed to delete a document access for a
document in which they are a simple reader or editor.
@@ -640,7 +636,7 @@ def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_get_
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -660,7 +656,7 @@ def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_get_
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_administrators_except_owners(
via, mock_user_get_teams
via, mock_user_teams
):
"""
Users who are administrators in a document should be allowed to delete an access
@@ -677,7 +673,7 @@ def test_api_document_accesses_delete_administrators_except_owners(
document=document, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="administrator"
)
@@ -698,7 +694,7 @@ def test_api_document_accesses_delete_administrators_except_owners(
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_get_teams):
def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_teams):
"""
Users who are administrators in a document should not be allowed to delete an ownership
access from the document.
@@ -714,7 +710,7 @@ def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_get
document=document, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="administrator"
)
@@ -733,7 +729,7 @@ def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_get
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_owners(via, mock_user_get_teams):
def test_api_document_accesses_delete_owners(via, mock_user_teams):
"""
Users should be able to delete the document access of another user
for a document of which they are owner.
@@ -747,7 +743,7 @@ def test_api_document_accesses_delete_owners(via, mock_user_get_teams):
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
@@ -766,7 +762,7 @@ def test_api_document_accesses_delete_owners(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_owners_last_owner(via, mock_user_get_teams):
def test_api_document_accesses_delete_owners_last_owner(via, mock_user_teams):
"""
It should not be possible to delete the last owner access from a document
"""
@@ -782,7 +778,7 @@ def test_api_document_accesses_delete_owners_last_owner(via, mock_user_get_teams
document=document, user=user, role="owner"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)

View File

@@ -66,7 +66,7 @@ def test_api_document_accesses_create_authenticated_unrelated():
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_create_authenticated_reader_or_editor(
via, role, mock_user_get_teams
via, role, mock_user_teams
):
"""Readers or editors of a document should not be allowed to create document accesses."""
user = factories.UserFactory()
@@ -78,7 +78,7 @@ def test_api_document_accesses_create_authenticated_reader_or_editor(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -101,9 +101,7 @@ def test_api_document_accesses_create_authenticated_reader_or_editor(
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_create_authenticated_administrator(
via, mock_user_get_teams
):
def test_api_document_accesses_create_authenticated_administrator(via, mock_user_teams):
"""
Administrators of a document should be able to create document accesses
except for the "owner" role.
@@ -120,7 +118,7 @@ def test_api_document_accesses_create_authenticated_administrator(
document=document, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="administrator"
)
@@ -178,7 +176,7 @@ def test_api_document_accesses_create_authenticated_administrator(
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_teams):
def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
"""
Owners of a document should be able to create document accesses whatever the role.
An email should be sent to the accesses to notify them of the adding.
@@ -192,7 +190,7 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_tea
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)

View File

@@ -80,7 +80,7 @@ def test_api_document_invitations__create__authenticated_outsider():
)
@pytest.mark.parametrize("via", VIA)
def test_api_document_invitations__create__privileged_members(
via, inviting, invited, is_allowed, mock_user_get_teams
via, inviting, invited, is_allowed, mock_user_teams
):
"""
Only owners and administrators should be able to invite new users.
@@ -91,7 +91,7 @@ def test_api_document_invitations__create__privileged_members(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=inviting)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=inviting
)
@@ -291,7 +291,7 @@ def test_api_document_invitations__list__anonymous_user():
@pytest.mark.parametrize("via", VIA)
def test_api_document_invitations__list__authenticated(
via, mock_user_get_teams, django_assert_num_queries
via, mock_user_teams, django_assert_num_queries
):
"""
Authenticated users should be able to list invitations for documents to which they are
@@ -304,7 +304,7 @@ def test_api_document_invitations__list__authenticated(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -432,7 +432,7 @@ def test_api_document_invitations__retrieve__unrelated_user():
@pytest.mark.parametrize("via", VIA)
def test_api_document_invitations__retrieve__document_member(via, mock_user_get_teams):
def test_api_document_invitations__retrieve__document_member(via, mock_user_teams):
"""
Authenticated users related to the document should be able to retrieve invitations
whatever their role in the document.
@@ -445,7 +445,7 @@ def test_api_document_invitations__retrieve__document_member(via, mock_user_get_
document=invitation.document, user=user, role=role
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role=role
)
@@ -475,7 +475,7 @@ def test_api_document_invitations__retrieve__document_member(via, mock_user_get_
@pytest.mark.parametrize("via", VIA)
def test_api_document_invitations__put_authenticated(via, mock_user_get_teams):
def test_api_document_invitations__put_authenticated(via, mock_user_teams):
"""
Authenticated user can put invitations.
"""
@@ -486,7 +486,7 @@ def test_api_document_invitations__put_authenticated(via, mock_user_get_teams):
document=invitation.document, user=user, role="owner"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role="owner"
)
@@ -503,7 +503,7 @@ def test_api_document_invitations__put_authenticated(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_document_invitations__patch_authenticated(via, mock_user_get_teams):
def test_api_document_invitations__patch_authenticated(via, mock_user_teams):
"""
Authenticated user can patch invitations.
"""
@@ -514,7 +514,7 @@ def test_api_document_invitations__patch_authenticated(via, mock_user_get_teams)
document=invitation.document, user=user, role="owner"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role="owner"
)
@@ -546,7 +546,7 @@ def test_api_document_invitations__patch_authenticated(via, mock_user_get_teams)
["editor", "reader"],
)
def test_api_document_invitations__update__forbidden__not_authenticated(
method, via, role, mock_user_get_teams
method, via, role, mock_user_teams
):
"""
Update of invitations is currently forbidden.
@@ -558,7 +558,7 @@ def test_api_document_invitations__update__forbidden__not_authenticated(
document=invitation.document, user=user, role=role
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role=role
)
@@ -607,7 +607,7 @@ def test_api_document_invitations__delete__authenticated_outsider():
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", ["owner", "administrator"])
def test_api_document_invitations__delete__privileged_members(
role, via, mock_user_get_teams
role, via, mock_user_teams
):
"""Privileged member should be able to cancel invitation."""
user = factories.UserFactory()
@@ -615,7 +615,7 @@ def test_api_document_invitations__delete__privileged_members(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -632,16 +632,14 @@ def test_api_document_invitations__delete__privileged_members(
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_document_invitations_delete_readers_or_editors(
via, role, mock_user_get_teams
):
def test_api_document_invitations_delete_readers_or_editors(via, role, mock_user_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=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)

View File

@@ -14,37 +14,29 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_document_versions_list_anonymous_public():
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
def test_api_document_versions_list_anonymous(role, reach):
"""
Anonymous users should not be allowed to list document versions for a public document.
Anonymous users should not be allowed to list document versions for a document
whatever the reach and role.
"""
document = factories.DocumentFactory(is_public=True)
factories.UserDocumentAccessFactory.create_batch(2, document=document)
document = factories.DocumentFactory(link_role=role, link_reach=reach)
# Accesses and traces for other users should not interfere
factories.UserDocumentAccessFactory(document=document)
models.LinkTrace.objects.create(document=document, user=factories.UserFactory())
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
assert response.status_code == 200
assert response.json() == []
def test_api_document_versions_list_anonymous_private():
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_document_versions_list_authenticated_unrelated(reach):
"""
Anonymous users should not be allowed to find document versions for a private document.
"""
document = factories.DocumentFactory(is_public=False)
factories.UserDocumentAccessFactory.create_batch(2, document=document)
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
def test_api_document_versions_list_authenticated_unrelated_public():
"""
Authenticated users should not be allowed to list document versions for a public document
Authenticated users should not be allowed to list document versions for a document
to which they are not related.
"""
user = factories.UserFactory()
@@ -52,7 +44,7 @@ def test_api_document_versions_list_authenticated_unrelated_public():
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach)
factories.UserDocumentAccessFactory.create_batch(3, document=document)
# The versions of another document to which the user is related should not be listed either
@@ -67,31 +59,8 @@ def test_api_document_versions_list_authenticated_unrelated_public():
}
def test_api_document_versions_list_authenticated_unrelated_private():
"""
Authenticated users should not be allowed to find document versions for a private document
to which they are not related.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
factories.UserDocumentAccessFactory.create_batch(3, document=document)
# The versions of another document to which the user is related should not be listed either
factories.UserDocumentAccessFactory(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/versions/",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
@pytest.mark.parametrize("via", VIA)
def test_api_document_versions_list_authenticated_related(via, mock_user_get_teams):
def test_api_document_versions_list_authenticated_related(via, mock_user_teams):
"""
Authenticated users should be able to list document versions for a document
to which they are directly related, whatever their role in the document.
@@ -109,7 +78,7 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_get_tea
role=random.choice(models.RoleChoices.choices)[0],
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
models.DocumentAccess.objects.create(
document=document,
team="lasuite",
@@ -143,11 +112,13 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_get_tea
assert content["count"] == 1
def test_api_document_versions_retrieve_anonymous_public():
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_document_versions_retrieve_anonymous(reach):
"""
Anonymous users should not be allowed to retrieve specific versions for a public document.
Anonymous users should not be allowed to find specific versions for a document with
restricted or authenticated link reach.
"""
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach)
version_id = document.get_versions_slice()["versions"][0]["version_id"]
url = f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/"
@@ -159,23 +130,10 @@ def test_api_document_versions_retrieve_anonymous_public():
}
def test_api_document_versions_retrieve_anonymous_private():
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_document_versions_retrieve_authenticated_unrelated(reach):
"""
Anonymous users should not be allowed to find specific versions for a private document.
"""
document = factories.DocumentFactory(is_public=False)
version_id = document.get_versions_slice()["versions"][0]["version_id"]
url = f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/"
response = APIClient().get(url)
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
def test_api_document_versions_retrieve_authenticated_unrelated_public():
"""
Authenticated users should not be allowed to retrieve specific versions for a public
Authenticated users should not be allowed to retrieve specific versions for a
document to which they are not related.
"""
user = factories.UserFactory()
@@ -183,7 +141,7 @@ def test_api_document_versions_retrieve_authenticated_unrelated_public():
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach)
version_id = document.get_versions_slice()["versions"][0]["version_id"]
response = client.get(
@@ -195,28 +153,8 @@ def test_api_document_versions_retrieve_authenticated_unrelated_public():
}
def test_api_document_versions_retrieve_authenticated_unrelated_private():
"""
Authenticated users should not be allowed to find specific versions for a private document
to which they are not related.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
version_id = document.get_versions_slice()["versions"][0]["version_id"]
response = client.get(
f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
@pytest.mark.parametrize("via", VIA)
def test_api_document_versions_retrieve_authenticated_related(via, mock_user_get_teams):
def test_api_document_versions_retrieve_authenticated_related(via, mock_user_teams):
"""
A user who is related to a document should be allowed to retrieve the
associated document user accesses.
@@ -232,10 +170,10 @@ def test_api_document_versions_retrieve_authenticated_related(via, mock_user_get
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
# Versions created before the document was shared should not be available to the user
# Versions created before the document was shared should not be seen by the user
response = client.get(
f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/",
)
@@ -267,10 +205,8 @@ def test_api_document_versions_create_anonymous():
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
assert response.status_code == 405
assert response.json() == {"detail": 'Method "POST" not allowed.'}
def test_api_document_versions_create_authenticated_unrelated():
@@ -295,7 +231,7 @@ def test_api_document_versions_create_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_document_versions_create_authenticated_related(via, mock_user_get_teams):
def test_api_document_versions_create_authenticated_related(via, mock_user_teams):
"""
Authenticated users related to a document should not be allowed to create document versions
whatever their role.
@@ -309,7 +245,7 @@ def test_api_document_versions_create_authenticated_related(via, mock_user_get_t
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
response = client.post(
@@ -331,7 +267,7 @@ def test_api_document_versions_update_anonymous():
{"foo": "bar"},
format="json",
)
assert response.status_code == 401
assert response.status_code == 405
def test_api_document_versions_update_authenticated_unrelated():
@@ -356,7 +292,7 @@ def test_api_document_versions_update_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_document_versions_update_authenticated_related(via, mock_user_get_teams):
def test_api_document_versions_update_authenticated_related(via, mock_user_teams):
"""
Authenticated users with access to a document should not be able to update its versions
whatever their role.
@@ -372,7 +308,7 @@ def test_api_document_versions_update_authenticated_related(via, mock_user_get_t
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
response = client.put(
@@ -397,7 +333,8 @@ def test_api_document_versions_delete_anonymous():
assert response.status_code == 401
def test_api_document_versions_delete_authenticated_public():
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_document_versions_delete_authenticated(reach):
"""
Authenticated users should not be allowed to delete a document version for a
public document to which they are not related.
@@ -407,7 +344,7 @@ def test_api_document_versions_delete_authenticated_public():
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach)
version_id = document.get_versions_slice()["versions"][0]["version_id"]
response = client.delete(
@@ -417,30 +354,9 @@ def test_api_document_versions_delete_authenticated_public():
assert response.status_code == 403
def test_api_document_versions_delete_authenticated_private():
"""
Authenticated users should not be allowed to find a document version to delete it
for a private document to which they are not related.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
version_id = document.get_versions_slice()["versions"][0]["version_id"]
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_get_teams):
def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_teams):
"""
Authenticated users should not be allowed to delete a document version for a
document in which they are a simple reader or editor.
@@ -454,7 +370,7 @@ def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_get_
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -484,7 +400,7 @@ def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_get_
@pytest.mark.parametrize("via", VIA)
def test_api_document_versions_delete_administrator_or_owner(via, mock_user_get_teams):
def test_api_document_versions_delete_administrator_or_owner(via, mock_user_teams):
"""
Users who are administrator or owner of a document should be allowed to delete a version.
"""
@@ -498,7 +414,7 @@ def test_api_document_versions_delete_administrator_or_owner(via, mock_user_get_
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)

View File

@@ -17,9 +17,22 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_documents_attachment_upload_anonymous():
"""Anonymous users can't upload attachments to a document."""
document = factories.DocumentFactory()
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_attachment_upload_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to upload attachments if the link reach
and role don't allow it.
"""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
@@ -31,16 +44,47 @@ def test_api_documents_attachment_upload_anonymous():
}
def test_api_documents_attachment_upload_authenticated_public():
def test_api_documents_attachment_upload_anonymous_success():
"""
Users who are not related to a public document should not be allowed to upload an attachment.
Anonymous users should be able to upload attachments to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = APIClient().post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.jpg")
match = pattern.search(response.json()["file"])
file_id = match.group(1)
# Validate that file_id is a valid UUID
uuid.UUID(file_id)
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("public", "reader"),
],
)
def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
"""
Users who are not related to a document can't upload attachments if the
link reach and role don't allow it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
@@ -52,27 +96,41 @@ def test_api_documents_attachment_upload_authenticated_public():
}
def test_api_documents_attachment_upload_authenticated_private():
@pytest.mark.parametrize(
"reach, role",
[
("authenticated", "editor"),
("public", "editor"),
],
)
def test_api_documents_attachment_upload_authenticated_success(reach, role):
"""
Users who are not related to a private document should not be able to upload an attachment.
Autenticated who are not related to a document should be able to upload a file
if the link reach and role permit it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.jpg")
match = pattern.search(response.json()["file"])
file_id = match.group(1)
# Validate that file_id is a valid UUID
uuid.UUID(file_id)
@pytest.mark.parametrize("via", VIA)
def test_api_documents_attachment_upload_reader(via, mock_user_get_teams):
def test_api_documents_attachment_upload_reader(via, mock_user_teams):
"""
Users who are simple readers on a document should not be allowed to upload an attachment.
"""
@@ -81,11 +139,11 @@ def test_api_documents_attachment_upload_reader(via, mock_user_get_teams):
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
document = factories.DocumentFactory(link_role="reader")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
@@ -103,7 +161,7 @@ def test_api_documents_attachment_upload_reader(via, mock_user_get_teams):
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_attachment_upload_success(via, role, mock_user_get_teams):
def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
"""
Editors, administrators and owners of a document should be able to upload an attachment.
"""
@@ -116,7 +174,7 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_get_teams)
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)

View File

@@ -2,7 +2,7 @@
Tests for Documents API endpoint in impress's core app: create
"""
import uuid
from uuid import uuid4
import pytest
from rest_framework.test import APIClient
@@ -26,7 +26,7 @@ def test_api_documents_create_anonymous():
assert not Document.objects.exists()
def test_api_documents_create_authenticated():
def test_api_documents_create_authenticated_success():
"""
Authenticated users should be able to create documents and should automatically be declared
as the owner of the newly created document.
@@ -50,24 +50,64 @@ def test_api_documents_create_authenticated():
assert document.accesses.filter(role="owner", user=user).exists()
def test_api_documents_create_with_id_from_payload():
"""
We should be able to create a document with an ID from the payload.
"""
def test_api_documents_create_authenticated_title_null():
"""It should be possible to create several documents with a null title."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
doc_id = uuid.uuid4()
factories.DocumentFactory(title=None)
response = client.post("/api/v1.0/documents/", {}, format="json")
assert response.status_code == 201
assert Document.objects.filter(title__isnull=True).count() == 2
def test_api_documents_create_force_id_success():
"""It should be possible to force the document ID when creating a document."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
forced_id = uuid4()
response = client.post(
"/api/v1.0/documents/",
{"title": "my document", "id": str(doc_id)},
{
"id": str(forced_id),
"title": "my document",
},
format="json",
)
assert response.status_code == 201
document = Document.objects.get()
assert document.title == "my document"
assert document.id == doc_id
assert document.accesses.filter(role="owner", user=user).exists()
documents = Document.objects.all()
assert len(documents) == 1
assert documents[0].id == forced_id
def test_api_documents_create_force_id_existing():
"""
It should not be possible to use the ID of an existing document when forcing ID on creation.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
response = client.post(
"/api/v1.0/documents/",
{
"id": str(document.id),
"title": "my document",
},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"id": ["A document with this ID already exists. You cannot override it."]
}

View File

@@ -2,8 +2,6 @@
Tests for Documents API endpoint in impress's core app: delete
"""
import random
import pytest
from rest_framework.test import APIClient
@@ -25,30 +23,31 @@ def test_api_documents_delete_anonymous():
assert models.Document.objects.count() == 1
def test_api_documents_delete_authenticated_unrelated():
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
def test_api_documents_delete_authenticated_unrelated(reach, role):
"""
Authenticated users should not be allowed to delete a document to which they are not
related.
Authenticated users should not be allowed to delete a document to which
they are not related.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
is_public = random.choice([True, False])
document = factories.DocumentFactory(is_public=is_public)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/",
)
assert response.status_code == 403 if is_public else 404
assert response.status_code == 403
assert models.Document.objects.count() == 1
@pytest.mark.parametrize("role", ["reader", "editor", "administrator"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_delete_authenticated_not_owner(via, role, mock_user_get_teams):
def test_api_documents_delete_authenticated_not_owner(via, role, mock_user_teams):
"""
Authenticated users should not be allowed to delete a document for which they are
only a reader, editor or administrator.
@@ -62,7 +61,7 @@ def test_api_documents_delete_authenticated_not_owner(via, role, mock_user_get_t
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -79,7 +78,7 @@ def test_api_documents_delete_authenticated_not_owner(via, role, mock_user_get_t
@pytest.mark.parametrize("via", VIA)
def test_api_documents_delete_authenticated_owner(via, mock_user_get_teams):
def test_api_documents_delete_authenticated_owner(via, mock_user_teams):
"""
Authenticated users should be able to delete a document they own.
"""
@@ -92,7 +91,7 @@ def test_api_documents_delete_authenticated_owner(via, mock_user_get_teams):
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)

View File

@@ -0,0 +1,152 @@
"""Tests for link configuration of documents on API endpoint"""
import pytest
from rest_framework.test import APIClient
from core import factories, models
from core.api import serializers
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_documents_link_configuration_update_anonymous(reach, role):
"""Anonymous users should not be allowed to update a link configuration."""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.LinkDocumentSerializer(instance=document).data
new_document_values = serializers.LinkDocumentSerializer(
instance=factories.DocumentFactory()
).data
response = APIClient().put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_document_values,
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
document.refresh_from_db()
document_values = serializers.LinkDocumentSerializer(instance=document).data
assert document_values == old_document_values
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_documents_link_configuration_update_authenticated_unrelated(reach, role):
"""
Authenticated users should not be allowed to update the link configuration for
a document to which they are not related.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.LinkDocumentSerializer(instance=document).data
new_document_values = serializers.LinkDocumentSerializer(
instance=factories.DocumentFactory()
).data
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_document_values,
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
document.refresh_from_db()
document_values = serializers.LinkDocumentSerializer(instance=document).data
assert document_values == old_document_values
@pytest.mark.parametrize("role", ["editor", "reader"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_link_configuration_update_authenticated_related_forbidden(
via, role, mock_user_teams
):
"""
Users who are readers or editors of a document should not be allowed to update
the link configuration.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
old_document_values = serializers.LinkDocumentSerializer(instance=document).data
new_document_values = serializers.LinkDocumentSerializer(
instance=factories.DocumentFactory()
).data
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_document_values,
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
document.refresh_from_db()
document_values = serializers.LinkDocumentSerializer(instance=document).data
assert document_values == old_document_values
@pytest.mark.parametrize("role", ["administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_link_configuration_update_authenticated_related_success(
via, role, mock_user_teams
):
"""
A user who is administrator or owner of a document should be allowed to update
the link configuration.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
new_document_values = serializers.LinkDocumentSerializer(
instance=factories.DocumentFactory()
).data
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_document_values,
format="json",
)
assert response.status_code == 200
document = models.Document.objects.get(pk=document.pk)
document_values = serializers.LinkDocumentSerializer(instance=document).data
for key, value in document_values.items():
assert value == new_document_values[key]

View File

@@ -2,68 +2,71 @@
Tests for Documents API endpoint in impress's core app: list
"""
import operator
from unittest import mock
import pytest
from faker import Faker
from rest_framework.pagination import PageNumberPagination
from rest_framework.status import HTTP_200_OK
from rest_framework.test import APIClient
from core import factories
from core import factories, models
fake = Faker()
pytestmark = pytest.mark.django_db
def test_api_documents_list_anonymous():
"""Anonymous users should only be able to list public documents."""
factories.DocumentFactory.create_batch(2, is_public=False)
documents = factories.DocumentFactory.create_batch(2, is_public=True)
expected_ids = {str(document.id) for document in documents}
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
def test_api_documents_list_anonymous(reach, role):
"""
Anonymous users should not be allowed to list documents whatever the
link reach and the role
"""
factories.DocumentFactory(link_reach=reach, link_role=role)
response = APIClient().get("/api/v1.0/documents/")
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 2
results_id = {result["id"] for result in results}
assert expected_ids == results_id
assert len(results) == 0
def test_api_documents_list_authenticated_direct():
"""
Authenticated users should be able to list documents they are a direct
owner/administrator/member of.
owner/administrator/member of or documents that have a link reach other
than restricted.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
related_documents = [
documents = [
access.document
for access in factories.UserDocumentAccessFactory.create_batch(5, user=user)
for access in factories.UserDocumentAccessFactory.create_batch(2, user=user)
]
public_documents = factories.DocumentFactory.create_batch(2, is_public=True)
factories.DocumentFactory.create_batch(2, is_public=False)
expected_ids = {
str(document.id) for document in related_documents + public_documents
}
# Unrelated and untraced documents
for reach in models.LinkReachChoices:
for role in models.LinkRoleChoices:
factories.DocumentFactory(link_reach=reach, link_role=role)
expected_ids = {str(document.id) for document in documents}
response = client.get(
"/api/v1.0/documents/",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 7
assert len(results) == 2
results_id = {result["id"] for result in results}
assert expected_ids == results_id
def test_api_documents_list_authenticated_via_team(mock_user_get_teams):
def test_api_documents_list_authenticated_via_team(mock_user_teams):
"""
Authenticated users should be able to list documents they are a
owner/administrator/member of via a team.
@@ -73,7 +76,7 @@ def test_api_documents_list_authenticated_via_team(mock_user_get_teams):
client = APIClient()
client.force_login(user)
mock_user_get_teams.return_value = ["team1", "team2", "unknown"]
mock_user_teams.return_value = ["team1", "team2", "unknown"]
documents_team1 = [
access.document
@@ -83,19 +86,71 @@ def test_api_documents_list_authenticated_via_team(mock_user_get_teams):
access.document
for access in factories.TeamDocumentAccessFactory.create_batch(3, team="team2")
]
public_documents = factories.DocumentFactory.create_batch(2, is_public=True)
factories.DocumentFactory.create_batch(2, is_public=False)
expected_ids = {
str(document.id)
for document in documents_team1 + documents_team2 + public_documents
}
expected_ids = {str(document.id) for document in documents_team1 + documents_team2}
response = client.get("/api/v1.0/documents/")
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 7
assert len(results) == 5
results_id = {result["id"] for result in results}
assert expected_ids == results_id
def test_api_documents_list_authenticated_link_reach_restricted():
"""
An authenticated user who has link traces to a document that is restricted should not
see it on the list view
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_traces=[user], link_reach="restricted")
# Link traces for other documents or other users should not interfere
models.LinkTrace.objects.create(document=document, user=factories.UserFactory())
other_document = factories.DocumentFactory(link_reach="public")
models.LinkTrace.objects.create(document=other_document, user=user)
response = client.get(
"/api/v1.0/documents/",
)
assert response.status_code == 200
results = response.json()["results"]
# Only the other document is returned but not the restricted document even though the user
# visited it earlier (probably b/c it previously had public or authenticated reach...)
assert len(results) == 1
assert results[0]["id"] == str(other_document.id)
def test_api_documents_list_authenticated_link_reach_public_or_authenticated():
"""
An authenticated user who has link traces to a document with public or authenticated
link reach should see it on the list view.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
documents = [
factories.DocumentFactory(link_traces=[user], link_reach=reach)
for reach in models.LinkReachChoices
if reach != "restricted"
]
expected_ids = {str(document.id) for document in documents}
response = client.get(
"/api/v1.0/documents/",
)
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 2
results_id = {result["id"] for result in results}
assert expected_ids == results_id
@@ -120,7 +175,7 @@ def test_api_documents_list_pagination(
"/api/v1.0/documents/",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
content = response.json()
assert content["count"] == 3
@@ -136,7 +191,7 @@ def test_api_documents_list_pagination(
"/api/v1.0/documents/?page=2",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
content = response.json()
assert content["count"] == 3
@@ -157,145 +212,63 @@ def test_api_documents_list_authenticated_distinct():
other_user = factories.UserFactory()
document = factories.DocumentFactory(users=[user, other_user], is_public=True)
document = factories.DocumentFactory(users=[user, other_user])
response = client.get(
"/api/v1.0/documents/",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
content = response.json()
assert len(content["results"]) == 1
assert content["results"][0]["id"] == str(document.id)
def test_api_documents_order_updated_at_desc_default():
"""
Test that the endpoint GET documents is sorted in 'updated_at' descending order by default.
"""
def test_api_documents_list_ordering_default():
"""Documents should be ordered by descending "updated_at" by default"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Updated at next year to ensure the order is correct
documents_updated = [
document.updated_at.isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(
5, is_public=True, updated_at=fake.date_time_this_year(before_now=False)
)
]
factories.DocumentFactory.create_batch(5, users=[user])
documents_updated.sort(reverse=True)
response = client.get("/api/v1.0/documents/")
response = APIClient().get(
"/api/v1.0/documents/",
)
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 5
response_data = response.json()
response_document_updated = [
document["updated_at"] for document in response_data["results"]
]
assert (
response_document_updated == documents_updated
), "updated_at values are not sorted from newest to oldest"
# Check that results are sorted by descending "updated_at" as expected
for i in range(4):
assert operator.ge(results[i]["updated_at"], results[i + 1]["updated_at"])
@pytest.mark.parametrize(
"ordering_field, factory_field",
[
("-created_at", "created_at"),
("-updated_at", "updated_at"),
("-title", "title"),
],
)
def test_api_documents_ordering_desc(ordering_field, factory_field):
"""
Test that the specified field is sorted in descending order
when the 'ordering' query parameter is set.
"""
def test_api_documents_list_ordering_by_fields():
"""It should be possible to order by several fields"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
if factory_field == "title":
documents_field_values = [
factories.DocumentFactory(
is_public=True, title=fake.sentence(nb_words=4)
).title
for _ in range(5)
]
else:
documents_field_values = [
getattr(document, factory_field).isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
factories.DocumentFactory.create_batch(5, users=[user])
documents_field_values.sort(reverse=True)
for parameter in [
"created_at",
"-created_at",
"updated_at",
"-updated_at",
"title",
"-title",
]:
is_descending = parameter.startswith("-")
field = parameter.lstrip("-")
querystring = f"?ordering={parameter}"
response = client.get(
f"/api/v1.0/documents/?ordering={ordering_field}"
if ordering_field != "-created_at"
else "/api/v1.0/documents/",
)
assert response.status_code == 200
response = client.get(f"/api/v1.0/documents/{querystring:s}")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 5
response_data = response.json()
response_documents_field_values = [
document[factory_field] for document in response_data["results"]
]
assert (
response_documents_field_values == documents_field_values
), f"{factory_field} values are not sorted as expected"
@pytest.mark.parametrize(
"field",
[
("updated_at"),
("title"),
("created_at"),
],
)
def test_api_documents_ordering_asc(field):
"""
Test that the specified field is sorted in ascending order
when the 'ordering' query parameter is set.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
if field == "title":
documents_field_values = [
factories.DocumentFactory(
is_public=True, title=fake.sentence(nb_words=4)
).title
for _ in range(5)
]
else:
documents_field_values = [
getattr(document, field).isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
documents_field_values.sort()
response = client.get(
f"/api/v1.0/documents/?ordering={field}",
)
assert response.status_code == 200
response_data = response.json()
response_documents_field_values = [
document[field] for document in response_data["results"]
]
assert (
response_documents_field_values == documents_field_values
), f"{field} values are not sorted as expected"
# Check that results are sorted by the field in querystring as expected
compare = operator.ge if is_descending else operator.le
for i in range(4):
assert compare(results[i][field], results[i + 1][field])

View File

@@ -13,7 +13,7 @@ pytestmark = pytest.mark.django_db
def test_api_documents_retrieve_anonymous_public():
"""Anonymous users should be allowed to retrieve public documents."""
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach="public")
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
@@ -21,36 +21,42 @@ def test_api_documents_retrieve_anonymous_public():
assert response.json() == {
"id": str(document.id),
"abilities": {
"attachment_upload": document.link_role == "editor",
"destroy": False,
"attachment_upload": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": False,
"partial_update": document.link_role == "editor",
"retrieve": True,
"update": False,
"update": document.link_role == "editor",
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
},
"accesses": [],
"link_reach": "public",
"link_role": document.link_role,
"title": document.title,
"is_public": True,
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}
def test_api_documents_retrieve_anonymous_not_public():
@pytest.mark.parametrize("reach", ["restricted", "authenticated"])
def test_api_documents_retrieve_anonymous_restricted_or_authenticated(reach):
"""Anonymous users should not be able to retrieve a document that is not public."""
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach=reach)
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_documents_retrieve_authenticated_unrelated_public():
@pytest.mark.parametrize("reach", ["public", "authenticated"])
def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(reach):
"""
Authenticated users should be able to retrieve a public document to which they are
not related.
@@ -60,7 +66,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public():
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
@@ -69,28 +75,30 @@ def test_api_documents_retrieve_authenticated_unrelated_public():
assert response.json() == {
"id": str(document.id),
"abilities": {
"attachment_upload": document.link_role == "editor",
"link_configuration": False,
"destroy": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"partial_update": document.link_role == "editor",
"retrieve": True,
"update": False,
"update": document.link_role == "editor",
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
},
"accesses": [],
"link_reach": reach,
"link_role": document.link_role,
"title": document.title,
"is_public": True,
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}
def test_api_documents_retrieve_authenticated_unrelated_not_public():
def test_api_documents_retrieve_authenticated_unrelated_restricted():
"""
Authenticated users should not be allowed to retrieve a document that is not public and
Authenticated users should not be allowed to retrieve a document that is restricted and
to which they are not related.
"""
user = factories.UserFactory()
@@ -98,13 +106,15 @@ def test_api_documents_retrieve_authenticated_unrelated_not_public():
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach="restricted")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
def test_api_documents_retrieve_authenticated_related_direct():
@@ -152,25 +162,26 @@ def test_api_documents_retrieve_authenticated_related_direct():
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_public": document.is_public,
"link_reach": document.link_reach,
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}
def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_teams):
def test_api_documents_retrieve_authenticated_related_team_none(mock_user_teams):
"""
Authenticated users should not be able to retrieve a document related to teams in
which the user is not.
Authenticated users should not be able to retrieve a restricted document related to
teams in which the user is not.
"""
mock_user_get_teams.return_value = []
mock_user_teams.return_value = []
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach="restricted")
factories.TeamDocumentAccessFactory(
document=document, team="readers", role="reader"
@@ -186,8 +197,10 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te
factories.TeamDocumentAccessFactory()
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize(
@@ -200,20 +213,20 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te
],
)
def test_api_documents_retrieve_authenticated_related_team_members(
teams, mock_user_get_teams
teams, mock_user_teams
):
"""
Authenticated users should be allowed to retrieve a document to which they
are related via a team whatever the role and see all its accesses.
"""
mock_user_get_teams.return_value = teams
mock_user_teams.return_value = teams
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach="restricted")
access_reader = factories.TeamDocumentAccessFactory(
document=document, team="readers", role="reader"
@@ -287,7 +300,8 @@ def test_api_documents_retrieve_authenticated_related_team_members(
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_public": False,
"link_reach": "restricted",
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}
@@ -302,20 +316,20 @@ def test_api_documents_retrieve_authenticated_related_team_members(
],
)
def test_api_documents_retrieve_authenticated_related_team_administrators(
teams, mock_user_get_teams
teams, mock_user_teams
):
"""
Authenticated users should be allowed to retrieve a document to which they
are related via a team whatever the role and see all its accesses.
"""
mock_user_get_teams.return_value = teams
mock_user_teams.return_value = teams
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach="restricted")
access_reader = factories.TeamDocumentAccessFactory(
document=document, team="readers", role="reader"
@@ -406,7 +420,8 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_public": False,
"link_reach": "restricted",
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}
@@ -422,20 +437,20 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
],
)
def test_api_documents_retrieve_authenticated_related_team_owners(
teams, mock_user_get_teams
teams, mock_user_teams
):
"""
Authenticated users should be allowed to retrieve a document to which they
are related via a team whatever the role and see all its accesses.
Authenticated users should be allowed to retrieve a restricted document to which
they are related via a team whatever the role and see all its accesses.
"""
mock_user_get_teams.return_value = teams
mock_user_teams.return_value = teams
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach="restricted")
access_reader = factories.TeamDocumentAccessFactory(
document=document, team="readers", role="reader"
@@ -529,7 +544,8 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_public": False,
"link_reach": "restricted",
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}

View File

@@ -22,7 +22,7 @@ pytestmark = pytest.mark.django_db
def test_api_documents_retrieve_auth_anonymous_public():
"""Anonymous users should be able to retrieve attachments linked to a public document"""
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach="public")
filename = f"{uuid.uuid4()!s}.jpg"
key = f"{document.pk!s}/attachments/{filename:s}"
@@ -64,12 +64,13 @@ def test_api_documents_retrieve_auth_anonymous_public():
assert response.content.decode("utf-8") == "my prose"
def test_api_documents_retrieve_auth_anonymous_not_public():
@pytest.mark.parametrize("reach", ["authenticated", "restricted"])
def test_api_documents_retrieve_auth_anonymous_authenticated_or_restricted(reach):
"""
Anonymous users should not be allowed to retrieve attachments linked to a document
that is not public.
with link reach set to authenticated or restricted.
"""
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach=reach)
filename = f"{uuid.uuid4()!s}.jpg"
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
@@ -82,12 +83,13 @@ def test_api_documents_retrieve_auth_anonymous_not_public():
assert "Authorization" not in response
def test_api_documents_retrieve_auth_authenticated_public():
@pytest.mark.parametrize("reach", ["public", "authenticated"])
def test_api_documents_retrieve_auth_authenticated_public_or_authenticated(reach):
"""
Authenticated users who are not related to a document should be able to
retrieve attachments linked to a public document.
Authenticated users who are not related to a document should be able to retrieve
attachments related to a document with public or authenticated link reach.
"""
document = factories.DocumentFactory(is_public=True)
document = factories.DocumentFactory(link_reach=reach)
user = factories.UserFactory()
client = APIClient()
@@ -104,7 +106,7 @@ def test_api_documents_retrieve_auth_authenticated_public():
)
original_url = f"http://localhost/media/{key:s}"
response = APIClient().get(
response = client.get(
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=original_url
)
@@ -133,12 +135,12 @@ def test_api_documents_retrieve_auth_authenticated_public():
assert response.content.decode("utf-8") == "my prose"
def test_api_documents_retrieve_auth_authenticated_not_public():
def test_api_documents_retrieve_auth_authenticated_restricted():
"""
Authenticated users who are not related to a document should not be allowed to
retrieve attachments linked to a document that is not public.
retrieve attachments linked to a document that is restricted.
"""
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach="restricted")
user = factories.UserFactory()
client = APIClient()
@@ -147,7 +149,7 @@ def test_api_documents_retrieve_auth_authenticated_not_public():
filename = f"{uuid.uuid4()!s}.jpg"
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
response = APIClient().get(
response = client.get(
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
)
@@ -155,22 +157,21 @@ def test_api_documents_retrieve_auth_authenticated_not_public():
assert "Authorization" not in response
@pytest.mark.parametrize("is_public", [True, False])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_retrieve_auth_related(via, is_public, mock_user_get_teams):
def test_api_documents_retrieve_auth_related(via, mock_user_teams):
"""
Users who have a role on a document, whatever the role, should be able to
Users who have a specific access to a document, whatever the role, should be able to
retrieve related attachments.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=is_public)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
filename = f"{uuid.uuid4()!s}.jpg"

View File

@@ -4,6 +4,8 @@ Tests for Documents API endpoint in impress's core app: update
import random
from django.contrib.auth.models import AnonymousUser
import pytest
from rest_framework.test import APIClient
@@ -14,9 +16,22 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_documents_update_anonymous():
"""Anonymous users should not be allowed to update a document."""
document = factories.DocumentFactory()
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_update_anonymous_forbidden(reach, role):
"""
Anonymous users should not be allowed to update a document when link
configuration does not allow it.
"""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.DocumentSerializer(instance=document).data
new_document_values = serializers.DocumentSerializer(
@@ -37,16 +52,26 @@ def test_api_documents_update_anonymous():
assert document_values == old_document_values
def test_api_documents_update_authenticated_unrelated():
@pytest.mark.parametrize(
"reach,role",
[
("public", "reader"),
("authenticated", "reader"),
("restricted", "reader"),
("restricted", "editor"),
],
)
def test_api_documents_update_authenticated_unrelated_forbidden(reach, role):
"""
Authenticated users should not be allowed to update a document to which they are not related.
Authenticated users should not be allowed to update a document to which
they are not related if the link configuration does not allow it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.DocumentSerializer(instance=document).data
new_document_values = serializers.DocumentSerializer(
@@ -58,18 +83,67 @@ def test_api_documents_update_authenticated_unrelated():
format="json",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
document.refresh_from_db()
document_values = serializers.DocumentSerializer(instance=document).data
assert document_values == old_document_values
@pytest.mark.parametrize("via", VIA)
def test_api_documents_update_authenticated_reader(via, mock_user_get_teams):
@pytest.mark.parametrize(
"is_authenticated,reach,role",
[
(False, "public", "editor"),
(True, "public", "editor"),
(True, "authenticated", "editor"),
],
)
def test_api_documents_update_anonymous_or_authenticated_unrelated(
is_authenticated, reach, role
):
"""
Users who are editors or reader of a document but not administrators should
Authenticated users should be able to update a document to which
they are not related if the link configuration allows it.
"""
client = APIClient()
if is_authenticated:
user = factories.UserFactory()
client.force_login(user)
else:
user = AnonymousUser()
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.DocumentSerializer(instance=document).data
new_document_values = serializers.DocumentSerializer(
instance=factories.DocumentFactory()
).data
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
new_document_values,
format="json",
)
assert response.status_code == 200
document = models.Document.objects.get(pk=document.pk)
document_values = serializers.DocumentSerializer(instance=document).data
for key, value in document_values.items():
if key in ["id", "accesses", "created_at", "link_reach", "link_role"]:
assert value == old_document_values[key]
elif key == "updated_at":
assert value > old_document_values[key]
else:
assert value == new_document_values[key]
@pytest.mark.parametrize("via", VIA)
def test_api_documents_update_authenticated_reader(via, mock_user_teams):
"""
Users who are reader of a document but not administrators should
not be allowed to update it.
"""
user = factories.UserFactory()
@@ -77,11 +151,11 @@ def test_api_documents_update_authenticated_reader(via, mock_user_get_teams):
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
document = factories.DocumentFactory(link_role="reader")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
@@ -110,7 +184,7 @@ def test_api_documents_update_authenticated_reader(via, mock_user_get_teams):
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_update_authenticated_editor_administrator_or_owner(
via, role, mock_user_get_teams
via, role, mock_user_teams
):
"""A user who is editor, administrator or owner of a document should be allowed to update it."""
user = factories.UserFactory()
@@ -122,7 +196,7 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -142,7 +216,7 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
document = models.Document.objects.get(pk=document.pk)
document_values = serializers.DocumentSerializer(instance=document).data
for key, value in document_values.items():
if key in ["id", "accesses", "created_at"]:
if key in ["id", "accesses", "created_at", "link_reach", "link_role"]:
assert value == old_document_values[key]
elif key == "updated_at":
assert value > old_document_values[key]
@@ -151,7 +225,7 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
@pytest.mark.parametrize("via", VIA)
def test_api_documents_update_authenticated_owners(via, mock_user_get_teams):
def test_api_documents_update_authenticated_owners(via, mock_user_teams):
"""Administrators of a document should be allowed to update it."""
user = factories.UserFactory()
@@ -162,7 +236,7 @@ def test_api_documents_update_authenticated_owners(via, mock_user_get_teams):
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
@@ -181,7 +255,7 @@ def test_api_documents_update_authenticated_owners(via, mock_user_get_teams):
document = models.Document.objects.get(pk=document.pk)
document_values = serializers.DocumentSerializer(instance=document).data
for key, value in document_values.items():
if key in ["id", "accesses", "created_at"]:
if key in ["id", "accesses", "created_at", "link_reach", "link_role"]:
assert value == old_document_values[key]
elif key == "updated_at":
assert value > old_document_values[key]
@@ -190,9 +264,7 @@ def test_api_documents_update_authenticated_owners(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_documents_update_administrator_or_owner_of_another(
via, mock_user_get_teams
):
def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_teams):
"""
Being administrator or owner of a document should not grant authorization to update
another document.
@@ -208,28 +280,27 @@ def test_api_documents_update_administrator_or_owner_of_another(
document=document, user=user, role=random.choice(["administrator", "owner"])
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document,
team="lasuite",
role=random.choice(["administrator", "owner"]),
)
is_public = random.choice([True, False])
document = factories.DocumentFactory(title="Old title", is_public=is_public)
old_document_values = serializers.DocumentSerializer(instance=document).data
other_document = factories.DocumentFactory(title="Old title", link_role="reader")
old_document_values = serializers.DocumentSerializer(instance=other_document).data
new_document_values = serializers.DocumentSerializer(
instance=factories.DocumentFactory()
).data
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
f"/api/v1.0/documents/{other_document.id!s}/",
new_document_values,
format="json",
)
assert response.status_code == 403 if is_public else 404
assert response.status_code == 403
document.refresh_from_db()
document_values = serializers.DocumentSerializer(instance=document).data
assert document_values == old_document_values
other_document.refresh_from_db()
other_document_values = serializers.DocumentSerializer(instance=other_document).data
assert other_document_values == old_document_values

View File

@@ -49,7 +49,7 @@ def test_api_templates_delete_authenticated_unrelated():
@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
via, role, mock_user_teams
):
"""
Authenticated users should not be allowed to delete a template for which they are
@@ -64,7 +64,7 @@ def test_api_templates_delete_authenticated_member_or_administrator(
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
@@ -81,7 +81,7 @@ def test_api_templates_delete_authenticated_member_or_administrator(
@pytest.mark.parametrize("via", VIA)
def test_api_templates_delete_authenticated_owner(via, mock_user_get_teams):
def test_api_templates_delete_authenticated_owner(via, mock_user_teams):
"""
Authenticated users should be able to delete a template they own.
"""
@@ -94,7 +94,7 @@ def test_api_templates_delete_authenticated_owner(via, mock_user_get_teams):
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)

View File

@@ -44,8 +44,10 @@ def test_api_templates_generate_document_anonymous_not_public():
format="json",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Template matches the given query."}
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_templates_generate_document_authenticated_public():
@@ -87,12 +89,14 @@ def test_api_templates_generate_document_authenticated_not_public():
format="json",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Template matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize("via", VIA)
def test_api_templates_generate_document_related(via, mock_user_get_teams):
def test_api_templates_generate_document_related(via, mock_user_teams):
"""Users related to a template can generate pdf document."""
user = factories.UserFactory()
@@ -102,7 +106,7 @@ def test_api_templates_generate_document_related(via, mock_user_get_teams):
if via == USER:
access = factories.UserTemplateAccessFactory(user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamTemplateAccessFactory(team="lasuite")
data = {"body": "# Test markdown body"}

View File

@@ -6,7 +6,6 @@ from unittest import mock
import pytest
from rest_framework.pagination import PageNumberPagination
from rest_framework.status import HTTP_200_OK
from rest_framework.test import APIClient
from core import factories
@@ -17,12 +16,12 @@ pytestmark = pytest.mark.django_db
def test_api_templates_list_anonymous():
"""Anonymous users should only be able to list public templates."""
factories.TemplateFactory.create_batch(2, is_public=False)
templates = factories.TemplateFactory.create_batch(2, is_public=True)
expected_ids = {str(template.id) for template in templates}
public_templates = factories.TemplateFactory.create_batch(2, is_public=True)
expected_ids = {str(template.id) for template in public_templates}
response = APIClient().get("/api/v1.0/templates/")
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 2
results_id = {result["id"] for result in results}
@@ -32,7 +31,7 @@ def test_api_templates_list_anonymous():
def test_api_templates_list_authenticated_direct():
"""
Authenticated users should be able to list templates they are a direct
owner/administrator/member of.
owner/administrator/member of or that are public.
"""
user = factories.UserFactory()
@@ -54,24 +53,24 @@ def test_api_templates_list_authenticated_direct():
"/api/v1.0/templates/",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 7
results_id = {result["id"] for result in results}
assert expected_ids == results_id
def test_api_templates_list_authenticated_via_team(mock_user_get_teams):
def test_api_templates_list_authenticated_via_team(mock_user_teams):
"""
Authenticated users should be able to list templates they are a
owner/administrator/member of via a team.
owner/administrator/member of via a team or that are public.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
mock_user_get_teams.return_value = ["team1", "team2", "unknown"]
mock_user_teams.return_value = ["team1", "team2", "unknown"]
templates_team1 = [
access.template
@@ -91,7 +90,7 @@ def test_api_templates_list_authenticated_via_team(mock_user_get_teams):
response = client.get("/api/v1.0/templates/")
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 7
results_id = {result["id"] for result in results}
@@ -118,7 +117,7 @@ def test_api_templates_list_pagination(
"/api/v1.0/templates/",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
content = response.json()
assert content["count"] == 3
@@ -134,7 +133,7 @@ def test_api_templates_list_pagination(
"/api/v1.0/templates/?page=2",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
content = response.json()
assert content["count"] == 3
@@ -161,26 +160,24 @@ def test_api_templates_list_authenticated_distinct():
"/api/v1.0/templates/",
)
assert response.status_code == HTTP_200_OK
assert response.status_code == 200
content = response.json()
assert len(content["results"]) == 1
assert content["results"][0]["id"] == str(template.id)
def test_api_templates_order():
"""
Test that the endpoint GET templates is sorted in 'created_at' descending order by default.
"""
def test_api_templates_list_order_default():
"""The templates list should be sorted by 'created_at' in descending order by default."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template_ids = [
str(template.id)
for template in factories.TemplateFactory.create_batch(5, is_public=True)
str(access.template.id)
for access in factories.UserTemplateAccessFactory.create_batch(5, user=user)
]
response = APIClient().get(
response = client.get(
"/api/v1.0/templates/",
)
@@ -195,21 +192,21 @@ def test_api_templates_order():
), "created_at values are not sorted from newest to oldest"
def test_api_templates_order_param():
def test_api_templates_list_order_param():
"""
Test that the 'created_at' field is sorted in ascending order
when the 'ordering' query parameter is set.
The templates list is sorted by 'created_at' in ascending order when setting
the "ordering" query parameter.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
templates_ids = [
str(template.id)
for template in factories.TemplateFactory.create_batch(5, is_public=True)
str(access.template.id)
for access in factories.UserTemplateAccessFactory.create_batch(5, user=user)
]
response = APIClient().get(
response = client.get(
"/api/v1.0/templates/?ordering=created_at",
)
assert response.status_code == 200

View File

@@ -41,8 +41,10 @@ def test_api_templates_retrieve_anonymous_not_public():
response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/")
assert response.status_code == 404
assert response.json() == {"detail": "No Template matches the given query."}
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_templates_retrieve_authenticated_unrelated_public():
@@ -94,8 +96,10 @@ def test_api_templates_retrieve_authenticated_unrelated_not_public():
response = client.get(
f"/api/v1.0/templates/{template.id!s}/",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Template matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
def test_api_templates_retrieve_authenticated_related_direct():
@@ -146,12 +150,12 @@ def test_api_templates_retrieve_authenticated_related_direct():
}
def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_teams):
def test_api_templates_retrieve_authenticated_related_team_none(mock_user_teams):
"""
Authenticated users should not be able to retrieve a template related to teams in
which the user is not.
"""
mock_user_get_teams.return_value = []
mock_user_teams.return_value = []
user = factories.UserFactory()
@@ -174,8 +178,10 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te
factories.TeamTemplateAccessFactory()
response = client.get(f"/api/v1.0/templates/{template.id!s}/")
assert response.status_code == 404
assert response.json() == {"detail": "No Template matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize(
@@ -188,13 +194,13 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te
],
)
def test_api_templates_retrieve_authenticated_related_team_readers_or_editors(
teams, mock_user_get_teams
teams, mock_user_teams
):
"""
Authenticated users should be allowed to retrieve a template to which they
are related via a team whatever the role and see all its accesses.
"""
mock_user_get_teams.return_value = teams
mock_user_teams.return_value = teams
user = factories.UserFactory()
@@ -287,13 +293,13 @@ def test_api_templates_retrieve_authenticated_related_team_readers_or_editors(
],
)
def test_api_templates_retrieve_authenticated_related_team_administrators(
teams, mock_user_get_teams
teams, mock_user_teams
):
"""
Authenticated users should be allowed to retrieve a template to which they
are related via a team whatever the role and see all its accesses.
"""
mock_user_get_teams.return_value = teams
mock_user_teams.return_value = teams
user = factories.UserFactory()
@@ -405,13 +411,13 @@ def test_api_templates_retrieve_authenticated_related_team_administrators(
],
)
def test_api_templates_retrieve_authenticated_related_team_owners(
teams, mock_user_get_teams
teams, mock_user_teams
):
"""
Authenticated users should be allowed to retrieve a template to which they
are related via a team whatever the role and see all its accesses.
"""
mock_user_get_teams.return_value = teams
mock_user_teams.return_value = teams
user = factories.UserFactory()

View File

@@ -58,8 +58,10 @@ def test_api_templates_update_authenticated_unrelated():
format="json",
)
assert response.status_code == 404
assert response.json() == {"detail": "No Template matches the given query."}
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
@@ -67,7 +69,7 @@ def test_api_templates_update_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_authenticated_readers(via, mock_user_get_teams):
def test_api_templates_update_authenticated_readers(via, mock_user_teams):
"""
Users who are readers of a template should not be allowed to update it.
"""
@@ -80,7 +82,7 @@ def test_api_templates_update_authenticated_readers(via, mock_user_get_teams):
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="reader")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="reader"
)
@@ -109,7 +111,7 @@ def test_api_templates_update_authenticated_readers(via, mock_user_get_teams):
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_authenticated_editor_or_administrator_or_owner(
via, role, mock_user_get_teams
via, role, mock_user_teams
):
"""Administrator or owner of a template should be allowed to update it."""
user = factories.UserFactory()
@@ -121,7 +123,7 @@ def test_api_templates_update_authenticated_editor_or_administrator_or_owner(
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
@@ -148,7 +150,7 @@ def test_api_templates_update_authenticated_editor_or_administrator_or_owner(
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_authenticated_owners(via, mock_user_get_teams):
def test_api_templates_update_authenticated_owners(via, mock_user_teams):
"""Administrators of a template should be allowed to update it."""
user = factories.UserFactory()
@@ -159,7 +161,7 @@ def test_api_templates_update_authenticated_owners(via, mock_user_get_teams):
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
@@ -185,9 +187,7 @@ def test_api_templates_update_authenticated_owners(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_administrator_or_owner_of_another(
via, mock_user_get_teams
):
def test_api_templates_update_administrator_or_owner_of_another(via, mock_user_teams):
"""
Being administrator or owner of a template should not grant authorization to update
another template.
@@ -203,7 +203,7 @@ def test_api_templates_update_administrator_or_owner_of_another(
template=template, user=user, role=random.choice(["administrator", "owner"])
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template,
team="lasuite",

View File

@@ -57,7 +57,7 @@ def test_api_template_accesses_list_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_list_authenticated_related(via, mock_user_get_teams):
def test_api_template_accesses_list_authenticated_related(via, mock_user_teams):
"""
Authenticated users should be able to list template accesses for a template
to which they are directly related, whatever their role in the template.
@@ -76,7 +76,7 @@ def test_api_template_accesses_list_authenticated_related(via, mock_user_get_tea
role=random.choice(models.RoleChoices.choices)[0],
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
user_access = models.TemplateAccess.objects.create(
template=template,
team="lasuite",
@@ -178,7 +178,7 @@ def test_api_template_accesses_retrieve_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_retrieve_authenticated_related(via, mock_user_get_teams):
def test_api_template_accesses_retrieve_authenticated_related(via, mock_user_teams):
"""
A user who is related to a template should be allowed to retrieve the
associated template user accesses.
@@ -192,7 +192,7 @@ def test_api_template_accesses_retrieve_authenticated_related(via, mock_user_get
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(template=template, team="lasuite")
access = factories.UserTemplateAccessFactory(template=template)
@@ -261,7 +261,7 @@ def test_api_template_accesses_create_authenticated_unrelated():
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_create_authenticated_editor_or_reader(
via, role, mock_user_get_teams
via, role, mock_user_teams
):
"""Editors or readers of a template should not be allowed to create template accesses."""
user = factories.UserFactory()
@@ -273,7 +273,7 @@ def test_api_template_accesses_create_authenticated_editor_or_reader(
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
@@ -296,9 +296,7 @@ def test_api_template_accesses_create_authenticated_editor_or_reader(
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_create_authenticated_administrator(
via, mock_user_get_teams
):
def test_api_template_accesses_create_authenticated_administrator(via, mock_user_teams):
"""
Administrators of a template should be able to create template accesses
except for the "owner" role.
@@ -314,7 +312,7 @@ def test_api_template_accesses_create_authenticated_administrator(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
@@ -363,7 +361,7 @@ def test_api_template_accesses_create_authenticated_administrator(
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_create_authenticated_owner(via, mock_user_get_teams):
def test_api_template_accesses_create_authenticated_owner(via, mock_user_teams):
"""
Owners of a template should be able to create template accesses whatever the role.
"""
@@ -376,7 +374,7 @@ def test_api_template_accesses_create_authenticated_owner(via, mock_user_get_tea
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
@@ -466,7 +464,7 @@ def test_api_template_accesses_update_authenticated_unrelated():
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_authenticated_editor_or_reader(
via, role, mock_user_get_teams
via, role, mock_user_teams
):
"""Editors or readers of a template should not be allowed to update its accesses."""
user = factories.UserFactory()
@@ -478,7 +476,7 @@ def test_api_template_accesses_update_authenticated_editor_or_reader(
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
@@ -506,9 +504,7 @@ def test_api_template_accesses_update_authenticated_editor_or_reader(
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_administrator_except_owner(
via, mock_user_get_teams
):
def test_api_template_accesses_update_administrator_except_owner(via, mock_user_teams):
"""
A user who is a direct administrator in a template should be allowed to update a user
access for this template, as long as they don't try to set the role to owner.
@@ -524,7 +520,7 @@ def test_api_template_accesses_update_administrator_except_owner(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
@@ -565,9 +561,7 @@ def test_api_template_accesses_update_administrator_except_owner(
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_administrator_from_owner(
via, mock_user_get_teams
):
def test_api_template_accesses_update_administrator_from_owner(via, mock_user_teams):
"""
A user who is an administrator in a template, should not be allowed to update
the user access of an "owner" for this template.
@@ -583,7 +577,7 @@ def test_api_template_accesses_update_administrator_from_owner(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
@@ -614,7 +608,7 @@ def test_api_template_accesses_update_administrator_from_owner(
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_administrator_to_owner(via, mock_user_get_teams):
def test_api_template_accesses_update_administrator_to_owner(via, mock_user_teams):
"""
A user who is an administrator in a template, should not be allowed to update
the user access of another user to grant template ownership.
@@ -630,7 +624,7 @@ def test_api_template_accesses_update_administrator_to_owner(via, mock_user_get_
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
@@ -668,7 +662,7 @@ def test_api_template_accesses_update_administrator_to_owner(via, mock_user_get_
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_owner(via, mock_user_get_teams):
def test_api_template_accesses_update_owner(via, mock_user_teams):
"""
A user who is an owner in a template should be allowed to update
a user access for this template whatever the role.
@@ -682,7 +676,7 @@ def test_api_template_accesses_update_owner(via, mock_user_get_teams):
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
@@ -724,7 +718,7 @@ def test_api_template_accesses_update_owner(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_owner_self(via, mock_user_get_teams):
def test_api_template_accesses_update_owner_self(via, mock_user_teams):
"""
A user who is owner of a template should be allowed to update
their own user access provided there are other owners in the template.
@@ -741,7 +735,7 @@ def test_api_template_accesses_update_owner_self(via, mock_user_get_teams):
template=template, user=user, role="owner"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
@@ -810,7 +804,7 @@ def test_api_template_accesses_delete_authenticated():
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_get_teams):
def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_teams):
"""
Authenticated users should not be allowed to delete a template access for a
template in which they are a simple editor or reader.
@@ -824,7 +818,7 @@ def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_get_
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
@@ -844,7 +838,7 @@ def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_get_
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_administrators_except_owners(
via, mock_user_get_teams
via, mock_user_teams
):
"""
Users who are administrators in a template should be allowed to delete an access
@@ -861,7 +855,7 @@ def test_api_template_accesses_delete_administrators_except_owners(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
@@ -882,7 +876,7 @@ def test_api_template_accesses_delete_administrators_except_owners(
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_get_teams):
def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_teams):
"""
Users who are administrators in a template should not be allowed to delete an ownership
access from the template.
@@ -898,7 +892,7 @@ def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_get
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
@@ -917,7 +911,7 @@ def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_get
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_owners(via, mock_user_get_teams):
def test_api_template_accesses_delete_owners(via, mock_user_teams):
"""
Users should be able to delete the template access of another user
for a template of which they are owner.
@@ -931,7 +925,7 @@ def test_api_template_accesses_delete_owners(via, mock_user_get_teams):
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
@@ -950,7 +944,7 @@ def test_api_template_accesses_delete_owners(via, mock_user_get_teams):
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_owners_last_owner(via, mock_user_get_teams):
def test_api_template_accesses_delete_owners_last_owner(via, mock_user_teams):
"""
It should not be possible to delete the last owner access from a template
"""
@@ -966,7 +960,7 @@ def test_api_template_accesses_delete_owners_last_owner(via, mock_user_get_teams
template=template, user=user, role="owner"
)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)

View File

@@ -27,15 +27,15 @@ def test_models_documents_id_unique():
def test_models_documents_title_null():
"""The "title" field should not be null."""
with pytest.raises(ValidationError, match="This field cannot be null."):
models.Document.objects.create(title=None)
"""The "title" field can be null."""
document = models.Document.objects.create(title=None)
assert document.title is None
def test_models_documents_title_empty():
"""The "title" field should not be empty."""
with pytest.raises(ValidationError, match="This field cannot be blank."):
models.Document.objects.create(title="")
"""The "title" field can be empty."""
document = models.Document.objects.create(title="")
assert document.title == ""
def test_models_documents_title_max_length():
@@ -57,30 +57,29 @@ def test_models_documents_file_key():
# get_abilities
def test_models_documents_get_abilities_anonymous_public():
"""Check abilities returned for an anonymous user if the document is public."""
document = factories.DocumentFactory(is_public=True)
abilities = document.get_abilities(AnonymousUser())
@pytest.mark.parametrize(
"is_authenticated,reach,role",
[
(True, "restricted", "reader"),
(True, "restricted", "editor"),
(False, "restricted", "reader"),
(False, "restricted", "editor"),
(False, "authenticated", "reader"),
(False, "authenticated", "editor"),
],
)
def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role):
"""
Check abilities returned for a document giving insufficient roles 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=role)
user = factories.UserFactory() if is_authenticated else AnonymousUser()
abilities = document.get_abilities(user)
assert abilities == {
"destroy": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
}
def test_models_documents_get_abilities_anonymous_not_public():
"""Check abilities returned for an anonymous user if the document is private."""
document = factories.DocumentFactory(is_public=False)
abilities = document.get_abilities(AnonymousUser())
assert abilities == {
"link_configuration": False,
"destroy": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": False,
@@ -91,13 +90,26 @@ def test_models_documents_get_abilities_anonymous_not_public():
}
def test_models_documents_get_abilities_authenticated_unrelated_public():
"""Check abilities returned for an authenticated user if the user is public."""
document = factories.DocumentFactory(is_public=True)
abilities = document.get_abilities(factories.UserFactory())
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_documents_get_abilities_reader(is_authenticated, reach):
"""
Check abilities returned for a document giving reader 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="reader")
user = factories.UserFactory() if is_authenticated else AnonymousUser()
abilities = document.get_abilities(user)
assert abilities == {
"destroy": False,
"attachment_upload": False,
"destroy": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
@@ -108,17 +120,30 @@ def test_models_documents_get_abilities_authenticated_unrelated_public():
}
def test_models_documents_get_abilities_authenticated_unrelated_not_public():
"""Check abilities returned for an authenticated user if the document is private."""
document = factories.DocumentFactory(is_public=False)
abilities = document.get_abilities(factories.UserFactory())
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_documents_get_abilities_editor(is_authenticated, reach):
"""
Check abilities returned for a document giving editor 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="editor")
user = factories.UserFactory() if is_authenticated else AnonymousUser()
abilities = document.get_abilities(user)
assert abilities == {
"attachment_upload": True,
"destroy": False,
"attachment_upload": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": False,
"update": False,
"partial_update": True,
"retrieve": True,
"update": True,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
@@ -131,8 +156,9 @@ def test_models_documents_get_abilities_owner():
access = factories.UserDocumentAccessFactory(role="owner", user=user)
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": True,
"attachment_upload": True,
"destroy": True,
"link_configuration": True,
"manage_accesses": True,
"partial_update": True,
"retrieve": True,
@@ -148,8 +174,9 @@ def test_models_documents_get_abilities_administrator():
access = factories.UserDocumentAccessFactory(role="administrator")
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": False,
"attachment_upload": True,
"destroy": False,
"link_configuration": True,
"manage_accesses": True,
"partial_update": True,
"retrieve": True,
@@ -168,8 +195,9 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": False,
"attachment_upload": True,
"destroy": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": True,
"retrieve": True,
@@ -182,14 +210,17 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
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")
access = factories.UserDocumentAccessFactory(
role="reader", document__link_role="reader"
)
with django_assert_num_queries(1):
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": False,
"attachment_upload": False,
"destroy": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
@@ -202,15 +233,18 @@ def test_models_documents_get_abilities_reader_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="reader")
access = factories.UserDocumentAccessFactory(
role="reader", document__link_role="reader"
)
access.document.user_roles = ["reader"]
with django_assert_num_queries(0):
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": False,
"attachment_upload": False,
"destroy": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,

View File

@@ -189,7 +189,7 @@ def test_models_document_invitations_get_abilities_authenticated():
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", ["administrator", "owner"])
def test_models_document_invitations_get_abilities_privileged_member(
role, via, mock_user_get_teams
role, via, mock_user_teams
):
"""Check abilities for a document member with a privileged role."""
@@ -198,7 +198,7 @@ def test_models_document_invitations_get_abilities_privileged_member(
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
@@ -217,7 +217,7 @@ def test_models_document_invitations_get_abilities_privileged_member(
@pytest.mark.parametrize("via", VIA)
def test_models_document_invitations_get_abilities_reader(via, mock_user_get_teams):
def test_models_document_invitations_get_abilities_reader(via, mock_user_teams):
"""Check abilities for a document reader with 'reader' role."""
user = factories.UserFactory()
@@ -225,7 +225,7 @@ def test_models_document_invitations_get_abilities_reader(via, mock_user_get_tea
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
@@ -242,7 +242,7 @@ def test_models_document_invitations_get_abilities_reader(via, mock_user_get_tea
@pytest.mark.parametrize("via", VIA)
def test_models_document_invitations_get_abilities_editor(via, mock_user_get_teams):
def test_models_document_invitations_get_abilities_editor(via, mock_user_teams):
"""Check abilities for a document editor with 'editor' role."""
user = factories.UserFactory()
@@ -250,7 +250,7 @@ def test_models_document_invitations_get_abilities_editor(via, mock_user_get_tea
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="editor")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="editor"
)

View File

@@ -130,7 +130,9 @@ def create_demo(stdout):
queue.push(
models.Document(
title=fake.sentence(nb_words=4),
is_public=random_true_with_probability(0.5),
link_reach=models.LinkReachChoices.AUTHENTICATED
if random_true_with_probability(0.5)
else random.choice(models.LinkReachChoices.values),
)
)

View File

@@ -164,6 +164,7 @@ export const mockedDocument = async (page: Page, json: object) => {
accesses: [],
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
@@ -172,7 +173,7 @@ export const mockedDocument = async (page: Page, json: object) => {
partial_update: false, // Means not editor
retrieve: true,
},
is_public: false,
link_reach: 'restricted',
created_at: '2021-09-01T09:00:00Z',
...json,
},

View File

@@ -136,6 +136,7 @@ test.describe('Doc Editor', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,

View File

@@ -34,6 +34,7 @@ test.describe('Doc Header', () => {
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
@@ -42,7 +43,7 @@ test.describe('Doc Header', () => {
partial_update: true,
retrieve: true,
},
is_public: true,
link_reach: 'public',
created_at: '2021-09-01T09:00:00Z',
});
@@ -153,6 +154,7 @@ test.describe('Doc Header', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
@@ -184,6 +186,7 @@ test.describe('Doc Header', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
@@ -215,6 +218,7 @@ test.describe('Doc Header', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,

View File

@@ -85,7 +85,7 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
$wrap="wrap"
>
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
<DocTagPublic />
<DocTagPublic doc={doc} />
<Text $size="s" $display="inline">
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
</Text>

View File

@@ -1,26 +1,18 @@
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import { Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { KEY_DOC_VISIBILITY, useDoc } from '@/features/docs/doc-management';
import { Doc, LinkReach } from '@/features/docs/doc-management';
export const DocTagPublic = () => {
interface DocTagPublicProps {
doc: Doc;
}
export const DocTagPublic = ({ doc }: DocTagPublicProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const {
query: { id },
} = useRouter();
const { data: doc } = useDoc(
{ id: id as string },
{
enabled: !!id,
queryKey: [KEY_DOC_VISIBILITY, { id }],
},
);
if (!doc?.is_public) {
if (doc?.link_reach !== LinkReach.PUBLIC) {
return null;
}

View File

@@ -1,3 +1,4 @@
export * from './useDoc';
export * from './useDocs';
export * from './useUpdateDoc';
export * from './useUpdateDocLink';

View File

@@ -4,7 +4,7 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs';
export type UpdateDocParams = Pick<Doc, 'id'> &
Partial<Pick<Doc, 'content' | 'title' | 'is_public'>>;
Partial<Pick<Doc, 'content' | 'title'>>;
export const updateDoc = async ({
id,

View File

@@ -0,0 +1,51 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs';
export type UpdateDocLinkParams = Pick<Doc, 'id'> &
Partial<Pick<Doc, 'link_role' | 'link_reach'>>;
export const updateDocLink = async ({
id,
...params
}: UpdateDocLinkParams): Promise<Doc> => {
const response = await fetchAPI(`documents/${id}/link-configuration/`, {
method: 'PUT',
body: JSON.stringify({
...params,
}),
});
if (!response.ok) {
throw new APIError(
'Failed to update the doc link',
await errorCauses(response),
);
}
return response.json() as Promise<Doc>;
};
interface UpdateDocLinkProps {
onSuccess?: (data: Doc) => void;
listInvalideQueries?: string[];
}
export function useUpdateDocLink({
onSuccess,
listInvalideQueries,
}: UpdateDocLinkProps = {}) {
const queryClient = useQueryClient();
return useMutation<Doc, APIError, UpdateDocLinkParams>({
mutationFn: updateDocLink,
onSuccess: (data) => {
listInvalideQueries?.forEach((queryKey) => {
void queryClient.resetQueries({
queryKey: [queryKey],
});
});
onSuccess?.(data);
},
});
}

View File

@@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next';
import { Box, Card, IconBG } from '@/components';
import { KEY_DOC_VISIBILITY, KEY_LIST_DOC, useUpdateDoc } from '../api';
import { Doc } from '../types';
import { KEY_DOC, KEY_LIST_DOC, useUpdateDocLink } from '../api';
import { Doc, LinkReach } from '../types';
interface DocVisibilityProps {
doc: Doc;
@@ -18,9 +18,11 @@ interface DocVisibilityProps {
export const DocVisibility = ({ doc }: DocVisibilityProps) => {
const { t } = useTranslation();
const [docPublic, setDocPublic] = useState(doc.is_public);
const [docPublic, setDocPublic] = useState(
doc.link_reach === LinkReach.PUBLIC,
);
const { toast } = useToastProvider();
const api = useUpdateDoc({
const api = useUpdateDocLink({
onSuccess: () => {
toast(
t('The document visiblitity has been updated.'),
@@ -30,13 +32,13 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
},
);
},
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC_VISIBILITY],
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
});
return (
<Card
$margin="tiny"
$padding="small"
$padding={{ horizontal: 'small', vertical: 'tiny' }}
aria-label={t('Doc visibility card')}
$direction="row"
$align="center"
@@ -50,10 +52,12 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
onChange={() => {
api.mutate({
id: doc.id,
is_public: !docPublic,
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
link_role: 'reader',
});
setDocPublic(!docPublic);
}}
disabled={!doc.abilities.link_configuration}
text={t(
docPublic
? 'Anyone on the internet with the link can view'

View File

@@ -78,7 +78,6 @@ export const ModalUpdateDoc = ({ onClose, doc }: ModalUpdateDocProps) => {
buttonText: t('Validate the modification'),
onClose,
initialTitle: doc.title,
isPublic: doc.is_public,
infoText: t('Enter the new name of the selected document.'),
titleModal: t('Update document "{{documentTitle}}"', {
documentTitle: doc.title,

View File

@@ -46,28 +46,26 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
onClose={onClose}
width="70vw"
$css="min-width: 320px;max-width: 777px;"
title={
<Card
$direction="row"
$align="center"
$margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }}
$padding="tiny"
$gap="1rem"
>
<Text $isMaterialIcon $size="48px" $theme="primary">
share
</Text>
<Box $align="flex-start">
<Text as="h3" $size="26px" $margin="none">
{t('Share')}
</Text>
<Text $size="small" $weight="normal" $textAlign="left">
{doc.title}
</Text>
</Box>
</Card>
}
>
<Card
$direction="row"
$align="center"
$margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }}
$padding="tiny"
$gap="1rem"
>
<Text $isMaterialIcon $size="48px" $theme="primary">
share
</Text>
<Box $align="flex-start">
<Text as="h3" $size="26px" $margin="none">
{t('Share')}
</Text>
<Text $size="small" $weight="normal" $textAlign="left">
{doc.title}
</Text>
</Box>
</Card>
<DocVisibility doc={doc} />
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
<InvitationList doc={doc} />

View File

@@ -20,18 +20,26 @@ export enum Role {
OWNER = 'owner',
}
export enum LinkReach {
RESTRICTED = 'restricted',
PUBLIC = 'public',
AUTHENTICATED = 'authenticated',
}
export type Base64 = string;
export interface Doc {
id: string;
title: string;
content: Base64;
is_public: boolean;
link_reach: LinkReach;
link_role: 'reader' | 'editor';
accesses: Access[];
created_at: string;
updated_at: string;
abilities: {
destroy: boolean;
link_configuration: boolean;
manage_accesses: boolean;
partial_update: boolean;
retrieve: boolean;

View File

@@ -8,6 +8,7 @@ import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
DocsOrdering,
LinkReach,
currentDocRole,
isDocsOrdering,
useDocs,
@@ -109,7 +110,7 @@ export const DocsGrid = () => {
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
{row.is_public && (
{row.link_reach === LinkReach.PUBLIC && (
<Text
$weight="bold"
$background={colorsTokens()['primary-600']}
@@ -117,7 +118,7 @@ export const DocsGrid = () => {
$padding="xtiny"
$radius="3px"
>
{row.is_public ? t('Public') : ''}
{t('Public')}
</Text>
)}
</StyledLink>

View File

@@ -98,7 +98,7 @@ export const InvitationList = ({ doc }: InvitationListProps) => {
<Card
$margin="tiny"
$padding="tiny"
$maxHeight="60%"
$maxHeight="40%"
$overflow="auto"
aria-label={t('List invitation card')}
>

View File

@@ -138,7 +138,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
return (
<Card
$gap="1rem"
$padding="1rem"
$padding={{ horizontal: 'small', vertical: 'tiny' }}
$margin="tiny"
$direction="row"
$align="center"

View File

@@ -93,7 +93,7 @@ export const MemberList = ({ doc }: MemberListProps) => {
<Card
$margin="tiny"
$padding="tiny"
$maxHeight="85%"
$maxHeight="69%"
$overflow="auto"
aria-label={t('List members card')}
>

View File

@@ -189,6 +189,7 @@ export class ApiPlugin implements WorkboxPlugin {
updated_at: new Date().toISOString(),
abilities: {
destroy: true,
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,

View File

@@ -1,7 +1,7 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Box, Text } from '@/components';
import { TextErrors } from '@/components/TextErrors';
@@ -31,7 +31,9 @@ interface DocProps {
}
const DocPage = ({ id }: DocProps) => {
const { data: doc, isLoading, isError, error } = useDoc({ id });
const { data: docQuery, isError, error } = useDoc({ id });
const [doc, setDoc] = useState(docQuery);
const navigate = useNavigate();
useEffect(() => {
@@ -42,6 +44,14 @@ const DocPage = ({ id }: DocProps) => {
}
}, [doc?.title]);
useEffect(() => {
if (!docQuery) {
return;
}
setDoc(docQuery);
}, [docQuery]);
if (isError && error) {
if (error.status === 404) {
navigate.replace(`/404`);
@@ -64,7 +74,7 @@ const DocPage = ({ id }: DocProps) => {
);
}
if (isLoading || !doc) {
if (!doc) {
return (
<Box $align="center" $justify="center" $height="100%">
<Loader />