mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 15:12:27 +02:00
Compare commits
10 Commits
production
...
feature/pu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eff663a78 | ||
|
|
feabab030d | ||
|
|
4002a049eb | ||
|
|
c7d8211aac | ||
|
|
25a24eaf12 | ||
|
|
9adecf93ec | ||
|
|
6a3a07db31 | ||
|
|
79b56ccd4e | ||
|
|
611d77f3bb | ||
|
|
de0acedc0a |
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
@@ -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])
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useDoc';
|
||||
export * from './useDocs';
|
||||
export * from './useUpdateDoc';
|
||||
export * from './useUpdateDocLink';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user