Compare commits

..

2 Commits

Author SHA1 Message Date
Anthony LC
8602a5dfe4 test 2025-11-19 13:11:18 +01:00
Anthony LC
5bc25cfa19 👷(CI) free disk space in some job
An error appeared in the CI indicating that
there is no space left on device on the
e2e tests jobs.
To fix this, we free some disk space
by removing unused docker images and some
unnecessary large folders.
2025-11-19 12:51:36 +01:00
81 changed files with 774 additions and 5711 deletions

View File

@@ -59,14 +59,6 @@ jobs:
-
name: Checkout repository
uses: actions/checkout@v4
- name: Checkout custom code repository
uses: actions/checkout@v4
with:
repository: 'AntoLC/docs-customized'
ref: 'main'
path: docs-custom
-
name: Docker meta
id: meta
@@ -84,7 +76,7 @@ jobs:
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production --build-arg CUSTOM_CODE=docs-custom'
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
-
name: Build and push
@@ -95,7 +87,6 @@ jobs:
target: frontend-production
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
CUSTOM_CODE=docs-custom
PUBLISH_AS_MIT=false
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -21,10 +21,10 @@ jobs:
shell: bash
run: |
set -e
HELMFILE=src/helm/helmfile.yaml.gotmpl
HELMFILE=src/helm/helmfile.yaml
environments=$(awk 'BEGIN {in_env=0} /^environments:/ {in_env=1; next} /^---/ {in_env=0} in_env && /^ [^ ]/ {gsub(/^ /,""); gsub(/:.*$/,""); print}' "$HELMFILE")
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env lint -f $HELMFILE || exit 1
helmfile -e $env -f $HELMFILE lint || exit 1
echo -e "\n"
done
done

View File

@@ -121,6 +121,12 @@ jobs:
- name: Set e2e env variables
run: cat env.d/development/common.e2e >> env.d/development/common.local
- name: Free disk space
run: |
docker system prune -af --volumes
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/share/boost
df -h
- name: Install Playwright Browsers
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright firefox webkit chromium

View File

@@ -6,46 +6,35 @@ and this project adheres to
## [Unreleased]
### Fixed
- ♿(frontend) improve accessibility:
- ♿(frontend) improve share modal button accessibility #1626
## [3.10.0] - 2025-11-18
### Added
- ✨(export) enable ODT export for documents #1524
- ✨(frontend) improve mobile UX by showing subdocs count #1540
### Changed
- ♻️(frontend) preserve @ character when esc is pressed after typing it #1512
- ♻️(frontend) make summary button fixed to remain visible during scroll #1581
- ♻️(frontend) pdf embed use full width #1526
### Fixed
- ♿(frontend) improve accessibility:
- ♿(frontend) improve ARIA in doc grid and editor for a11y #1519
- ♿(frontend) improve accessibility and styling of summary table #1528
- ♿(frontend) add focus trap and enter key support to remove doc modal #1531
- 🐛(frontend) preserve @ character when esc is pressed after typing it #1512
- 🐛(frontend) make summary button fixed to remain visible during scroll #1581
- 🐛(frontend) fix pdf embed to use full width #1526
- 🐛(frontend) fix alignment of side menu #1597
- 🐛(frontend) fix fallback translations with Trans #1620
- 🐛(export) fix image overflow by limiting width to 600px during export #1525
- 🐛(export) fix table cell alignment issue in exported documents #1582
- 🐛(export) preserve image aspect ratio in PDF export #1622
- 🐛(export) Export fails when paste with style #1552
- 👷(CI) free disk space in some jobs #1635
### Security
- mitigate role escalation in the ask_for_access viewset #1580
- 🐛(frontend) preserve left panel width on window resize #1588
### Removed
- 🔥(backend) remove api managing templates
- 🔥(backend) remove api managing templates #1590
## [3.9.0] - 2025-11-10
@@ -54,7 +43,6 @@ and this project adheres to
- ✨(frontend) create skeleton component for DocEditor #1491
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
- ✨(frontend) ajustable left panel #1456
- ✨ Add comments feature to the editor #1330
### Changed
@@ -66,6 +54,7 @@ and this project adheres to
- 🐛(frontend) fix duplicate document entries in grid #1479
- 🐛(backend) fix trashbin list #1520
- ♿(frontend) improve accessibility:
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
- 🐛(backend) fix s3 version_id validation #1543
@@ -183,7 +172,6 @@ and this project adheres to
### Added
- ✨(backend) Comments on text editor #1309
- 👷(CI) add bundle size check job #1268
- ✨(frontend) use title first emoji as doc icon in tree #1289
@@ -886,8 +874,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.10.0...main
[v3.10.0]: https://github.com/suitenumerique/docs/releases/v3.10.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.9.0...main
[v3.9.0]: https://github.com/suitenumerique/docs/releases/v3.9.0
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1

View File

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

View File

@@ -1,5 +1,4 @@
"""Client serializers for the impress core app."""
# pylint: disable=too-many-lines
import binascii
import mimetypes
@@ -25,13 +24,22 @@ from core.services.converter_services import (
class UserSerializer(serializers.ModelSerializer):
"""Serialize users."""
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]
class UserLightSerializer(UserSerializer):
"""Serialize users with limited fields."""
full_name = serializers.SerializerMethodField(read_only=True)
short_name = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]
fields = ["full_name", "short_name"]
read_only_fields = ["full_name", "short_name"]
def get_full_name(self, instance):
"""Return the full name of the user."""
@@ -50,15 +58,6 @@ class UserSerializer(serializers.ModelSerializer):
return instance.short_name
class UserLightSerializer(UserSerializer):
"""Serialize users with limited fields."""
class Meta:
model = models.User
fields = ["full_name", "short_name"]
read_only_fields = ["full_name", "short_name"]
class TemplateAccessSerializer(serializers.ModelSerializer):
"""Serialize template accesses."""
@@ -892,124 +891,3 @@ class MoveDocumentSerializer(serializers.Serializer):
choices=enums.MoveNodePositionChoices.choices,
default=enums.MoveNodePositionChoices.LAST_CHILD,
)
class ReactionSerializer(serializers.ModelSerializer):
"""Serialize reactions."""
users = UserLightSerializer(many=True, read_only=True)
class Meta:
model = models.Reaction
fields = [
"id",
"emoji",
"created_at",
"users",
]
read_only_fields = ["id", "created_at", "users"]
class CommentSerializer(serializers.ModelSerializer):
"""Serialize comments (nested under a thread) with reactions and abilities."""
user = UserLightSerializer(read_only=True)
abilities = serializers.SerializerMethodField()
reactions = ReactionSerializer(many=True, read_only=True)
class Meta:
model = models.Comment
fields = [
"id",
"user",
"body",
"created_at",
"updated_at",
"reactions",
"abilities",
]
read_only_fields = [
"id",
"user",
"created_at",
"updated_at",
"reactions",
"abilities",
]
def validate(self, attrs):
"""Validate comment data."""
request = self.context.get("request")
user = getattr(request, "user", None)
attrs["thread_id"] = self.context["thread_id"]
attrs["user_id"] = user.id if user else None
return attrs
def get_abilities(self, obj):
"""Return comment's abilities."""
request = self.context.get("request")
if request:
return obj.get_abilities(request.user)
return {}
class ThreadSerializer(serializers.ModelSerializer):
"""Serialize threads in a backward compatible shape for current frontend.
We expose a flatten representation where ``content`` maps to the first
comment's body. Creating a thread requires a ``content`` field which is
stored as the first comment.
"""
creator = UserLightSerializer(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
body = serializers.JSONField(write_only=True, required=True)
comments = serializers.SerializerMethodField(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = models.Thread
fields = [
"id",
"body",
"created_at",
"updated_at",
"creator",
"abilities",
"comments",
"resolved",
"resolved_at",
"resolved_by",
"metadata",
]
read_only_fields = [
"id",
"created_at",
"updated_at",
"creator",
"abilities",
"comments",
"resolved",
"resolved_at",
"resolved_by",
"metadata",
]
def validate(self, attrs):
"""Validate thread data."""
request = self.context.get("request")
user = getattr(request, "user", None)
attrs["document_id"] = self.context["resource_id"]
attrs["creator_id"] = user.id if user else None
return attrs
def get_abilities(self, thread):
"""Return thread's abilities."""
request = self.context.get("request")
if request:
return thread.get_abilities(request.user)
return {}

View File

@@ -21,7 +21,6 @@ from django.db.models.expressions import RawSQL
from django.db.models.functions import Left, Length
from django.http import Http404, StreamingHttpResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.text import capfirst, slugify
from django.utils.translation import gettext_lazy as _
@@ -2151,132 +2150,3 @@ class ConfigView(drf.views.APIView):
)
return theme_customization
class CommentViewSetMixin:
"""Comment ViewSet Mixin."""
_document = None
def get_document_or_404(self):
"""Get the document related to the viewset or raise a 404 error."""
if self._document is None:
try:
self._document = models.Document.objects.get(
pk=self.kwargs["resource_id"],
)
except models.Document.DoesNotExist as e:
raise drf.exceptions.NotFound("Document not found.") from e
return self._document
class ThreadViewSet(
ResourceAccessViewsetMixin,
CommentViewSetMixin,
drf.mixins.CreateModelMixin,
drf.mixins.ListModelMixin,
drf.mixins.RetrieveModelMixin,
drf.mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""Thread API: list/create threads and nested comment operations."""
permission_classes = [permissions.CommentPermission]
pagination_class = Pagination
serializer_class = serializers.ThreadSerializer
queryset = models.Thread.objects.select_related("creator", "document").filter(
resolved=False
)
resource_field_name = "document"
def perform_create(self, serializer):
"""Create the first comment of the thread."""
body = serializer.validated_data["body"]
del serializer.validated_data["body"]
thread = serializer.save()
models.Comment.objects.create(
thread=thread,
user=self.request.user if self.request.user.is_authenticated else None,
body=body,
)
@drf.decorators.action(detail=True, methods=["post"], url_path="resolve")
def resolve(self, request, *args, **kwargs):
"""Resolve a thread."""
thread = self.get_object()
if not thread.resolved:
thread.resolved = True
thread.resolved_at = timezone.now()
thread.resolved_by = request.user
thread.save(update_fields=["resolved", "resolved_at", "resolved_by"])
return drf.response.Response(status=status.HTTP_204_NO_CONTENT)
class CommentViewSet(
CommentViewSetMixin,
viewsets.ModelViewSet,
):
"""Comment API: list/create comments and nested reaction operations."""
permission_classes = [permissions.CommentPermission]
pagination_class = Pagination
serializer_class = serializers.CommentSerializer
queryset = models.Comment.objects.select_related("user").all()
def get_queryset(self):
"""Override to filter on related resource."""
return (
super()
.get_queryset()
.filter(
thread=self.kwargs["thread_id"],
thread__document=self.kwargs["resource_id"],
)
)
def get_serializer_context(self):
"""Extra context provided to the serializer class."""
context = super().get_serializer_context()
context["document_id"] = self.kwargs["resource_id"]
context["thread_id"] = self.kwargs["thread_id"]
return context
@drf.decorators.action(
detail=True,
methods=["post", "delete"],
)
def reactions(self, request, *args, **kwargs):
"""POST: add reaction; DELETE: remove reaction.
Emoji is expected in request.data['emoji'] for both operations.
"""
comment = self.get_object()
serializer = serializers.ReactionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
if request.method == "POST":
reaction, created = models.Reaction.objects.get_or_create(
comment=comment,
emoji=serializer.validated_data["emoji"],
)
if not created and reaction.users.filter(id=request.user.id).exists():
return drf.response.Response(
{"user_already_reacted": True}, status=status.HTTP_400_BAD_REQUEST
)
reaction.users.add(request.user)
return drf.response.Response(status=status.HTTP_201_CREATED)
# DELETE
try:
reaction = models.Reaction.objects.get(
comment=comment,
emoji=serializer.validated_data["emoji"],
users__in=[request.user],
)
except models.Reaction.DoesNotExist as e:
raise drf.exceptions.NotFound("Reaction not found.") from e
reaction.users.remove(request.user)
if not reaction.users.exists():
reaction.delete()
return drf.response.Response(status=status.HTTP_204_NO_CONTENT)

View File

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

View File

@@ -256,49 +256,3 @@ class InvitationFactory(factory.django.DjangoModelFactory):
document = factory.SubFactory(DocumentFactory)
role = factory.fuzzy.FuzzyChoice([role[0] for role in models.RoleChoices.choices])
issuer = factory.SubFactory(UserFactory)
class ThreadFactory(factory.django.DjangoModelFactory):
"""A factory to create threads for a document"""
class Meta:
model = models.Thread
document = factory.SubFactory(DocumentFactory)
creator = factory.SubFactory(UserFactory)
class CommentFactory(factory.django.DjangoModelFactory):
"""A factory to create comments for a thread"""
class Meta:
model = models.Comment
thread = factory.SubFactory(ThreadFactory)
user = factory.SubFactory(UserFactory)
body = factory.Faker("text")
class ReactionFactory(factory.django.DjangoModelFactory):
"""A factory to create reactions for a comment"""
class Meta:
model = models.Reaction
comment = factory.SubFactory(CommentFactory)
emoji = "test"
@factory.post_generation
def users(self, create, extracted, **kwargs):
"""Add users to reaction from a given list of users or create one if not provided."""
if not create:
return
if not extracted:
# the factory is being created, but no users were provided
user = UserFactory()
self.users.add(user)
return
# Add the iterable of groups using bulk addition
self.users.add(*extracted)

View File

@@ -1,275 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-16 08:59
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0025_alter_user_short_name"),
]
operations = [
migrations.AlterField(
model_name="document",
name="link_role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commenter", "Commenter"),
("editor", "Editor"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="documentaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commenter", "Commenter"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="documentaskforaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commenter", "Commenter"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="invitation",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commenter", "Commenter"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="templateaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commenter", "Commenter"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.CreateModel(
name="Thread",
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",
),
),
("resolved", models.BooleanField(default=False)),
("resolved_at", models.DateTimeField(blank=True, null=True)),
("metadata", models.JSONField(blank=True, default=dict)),
(
"creator",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="threads",
to=settings.AUTH_USER_MODEL,
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="threads",
to="core.document",
),
),
(
"resolved_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resolved_threads",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Thread",
"verbose_name_plural": "Threads",
"db_table": "impress_thread",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name="Comment",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("body", models.JSONField()),
("metadata", models.JSONField(blank=True, default=dict)),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="thread_comment",
to=settings.AUTH_USER_MODEL,
),
),
(
"thread",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="core.thread",
),
),
],
options={
"verbose_name": "Comment",
"verbose_name_plural": "Comments",
"db_table": "impress_comment",
"ordering": ("created_at",),
},
),
migrations.CreateModel(
name="Reaction",
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",
),
),
("emoji", models.CharField(max_length=32)),
(
"comment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reactions",
to="core.comment",
),
),
(
"users",
models.ManyToManyField(
related_name="reactions", to=settings.AUTH_USER_MODEL
),
),
],
options={
"verbose_name": "Reaction",
"verbose_name_plural": "Reactions",
"db_table": "impress_comment_reaction",
"constraints": [
models.UniqueConstraint(
fields=("comment", "emoji"),
name="unique_comment_emoji",
violation_error_message="This emoji has already been reacted to this comment.",
)
],
},
),
]

View File

@@ -756,7 +756,6 @@ class Document(MP_Node, BaseModel):
can_update = (
is_owner_or_admin or role == RoleChoices.EDITOR
) and not is_deleted
can_comment = (can_update or role == RoleChoices.COMMENTER) and not is_deleted
can_create_children = can_update and user.is_authenticated
can_destroy = (
is_owner
@@ -787,7 +786,6 @@ class Document(MP_Node, BaseModel):
"children_list": can_get,
"children_create": can_create_children,
"collaboration_auth": can_get,
"comment": can_comment,
"content": can_get,
"cors_proxy": can_get,
"descendants": can_get,
@@ -1148,12 +1146,7 @@ class DocumentAccess(BaseAccess):
set_role_to = []
if is_owner_or_admin:
set_role_to.extend(
[
RoleChoices.READER,
RoleChoices.COMMENTER,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]
[RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN]
)
if role == RoleChoices.OWNER:
set_role_to.append(RoleChoices.OWNER)
@@ -1277,153 +1270,6 @@ class DocumentAskForAccess(BaseModel):
self.document.send_email(subject, [email], context, language)
class Thread(BaseModel):
"""Discussion thread attached to a document.
A thread groups one or many comments. For backward compatibility with the
existing frontend (useComments hook) we still expose a flattened serializer
that returns a "content" field representing the first comment's body.
"""
document = models.ForeignKey(
Document,
on_delete=models.CASCADE,
related_name="threads",
)
creator = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="threads",
null=True,
blank=True,
)
resolved = models.BooleanField(default=False)
resolved_at = models.DateTimeField(null=True, blank=True)
resolved_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="resolved_threads",
null=True,
blank=True,
)
metadata = models.JSONField(default=dict, blank=True)
class Meta:
db_table = "impress_thread"
ordering = ("-created_at",)
verbose_name = _("Thread")
verbose_name_plural = _("Threads")
def __str__(self):
author = self.creator or _("Anonymous")
return f"Thread by {author!s} on {self.document!s}"
def get_abilities(self, user):
"""Compute and return abilities for a given user (mirrors comment logic)."""
role = self.document.get_role(user)
doc_abilities = self.document.get_abilities(user)
read_access = doc_abilities.get("comment", False)
write_access = self.creator == user or role in [
RoleChoices.OWNER,
RoleChoices.ADMIN,
]
return {
"destroy": write_access,
"update": write_access,
"partial_update": write_access,
"resolve": write_access,
"retrieve": read_access,
}
@property
def first_comment(self):
"""Return the first createdcomment of the thread."""
return self.comments.order_by("created_at").first()
class Comment(BaseModel):
"""A comment belonging to a thread."""
thread = models.ForeignKey(
Thread,
on_delete=models.CASCADE,
related_name="comments",
)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="thread_comment",
null=True,
blank=True,
)
body = models.JSONField()
metadata = models.JSONField(default=dict, blank=True)
class Meta:
db_table = "impress_comment"
ordering = ("created_at",)
verbose_name = _("Comment")
verbose_name_plural = _("Comments")
def __str__(self):
"""Return the string representation of the comment."""
author = self.user or _("Anonymous")
return f"Comment by {author!s} on thread {self.thread_id}"
def get_abilities(self, user):
"""Return the abilities of the comment."""
role = self.thread.document.get_role(user)
doc_abilities = self.thread.document.get_abilities(user)
read_access = doc_abilities.get("comment", False)
can_react = read_access and user.is_authenticated
write_access = self.user == user or role in [
RoleChoices.OWNER,
RoleChoices.ADMIN,
]
return {
"destroy": write_access,
"update": write_access,
"partial_update": write_access,
"reactions": can_react,
"retrieve": read_access,
}
class Reaction(BaseModel):
"""Aggregated reactions for a given emoji on a comment.
We store one row per (comment, emoji) and maintain the list of user IDs who
reacted with that emoji. This matches the frontend interface where a
reaction exposes: emoji, createdAt (first reaction date) and userIds.
"""
comment = models.ForeignKey(
Comment,
on_delete=models.CASCADE,
related_name="reactions",
)
emoji = models.CharField(max_length=32)
users = models.ManyToManyField(User, related_name="reactions")
class Meta:
db_table = "impress_comment_reaction"
constraints = [
models.UniqueConstraint(
fields=["comment", "emoji"],
name="unique_comment_emoji",
violation_error_message=_(
"This emoji has already been reacted to this comment."
),
),
]
verbose_name = _("Reaction")
verbose_name_plural = _("Reactions")
def __str__(self):
"""Return the string representation of the reaction."""
return f"Reaction {self.emoji} on comment {self.comment.id}"
class Template(BaseModel):
"""HTML and CSS code used for formatting the print around the MarkDown body."""

View File

@@ -293,7 +293,6 @@ def test_api_document_accesses_retrieve_set_role_to_child():
}
assert result_dict[str(document_access_other_user.id)] == [
"reader",
"commenter",
"editor",
"administrator",
"owner",
@@ -302,7 +301,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
# Add an access for the other user on the parent
parent_access_other_user = factories.UserDocumentAccessFactory(
document=parent, user=other_user, role="commenter"
document=parent, user=other_user, role="editor"
)
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
@@ -315,7 +314,6 @@ def test_api_document_accesses_retrieve_set_role_to_child():
result["id"]: result["abilities"]["set_role_to"] for result in content
}
assert result_dict[str(document_access_other_user.id)] == [
"commenter",
"editor",
"administrator",
"owner",
@@ -323,7 +321,6 @@ def test_api_document_accesses_retrieve_set_role_to_child():
assert result_dict[str(parent_access.id)] == []
assert result_dict[str(parent_access_other_user.id)] == [
"reader",
"commenter",
"editor",
"administrator",
"owner",
@@ -336,28 +333,28 @@ def test_api_document_accesses_retrieve_set_role_to_child():
[
["administrator", "reader", "reader", "reader"],
[
["reader", "commenter", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
[],
["reader", "commenter", "editor", "administrator"],
["reader", "editor", "administrator"],
],
],
[
["owner", "reader", "reader", "reader"],
[
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["owner", "reader", "reader", "owner"],
[
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
],
@@ -418,44 +415,44 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
[
["administrator", "reader", "reader", "reader"],
[
["reader", "commenter", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
[],
["reader", "commenter", "editor", "administrator"],
["reader", "editor", "administrator"],
],
],
[
["owner", "reader", "reader", "reader"],
[
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["owner", "reader", "reader", "owner"],
[
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["reader", "reader", "reader", "owner"],
[
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commenter", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["reader", "administrator", "reader", "editor"],
[
["reader", "commenter", "editor", "administrator"],
["reader", "commenter", "editor", "administrator"],
["reader", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
[],
],
@@ -463,7 +460,7 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
[
["editor", "editor", "administrator", "editor"],
[
["reader", "commenter", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
["editor", "administrator"],
[],

View File

@@ -360,7 +360,6 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role):
expected_set_role_to = [
RoleChoices.READER,
RoleChoices.COMMENTER,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]
@@ -481,7 +480,6 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role):
assert response.status_code == 200
expected_set_role_to = [
RoleChoices.READER,
RoleChoices.COMMENTER,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]

View File

@@ -1,878 +0,0 @@
"""Test API for comments on documents."""
import random
from django.contrib.auth.models import AnonymousUser
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
# List comments
def test_list_comments_anonymous_user_public_document():
"""Anonymous users should be allowed to list comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
comment1, comment2 = factories.CommentFactory.create_batch(2, thread=thread)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
response = APIClient().get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
)
assert response.status_code == 200
assert response.json() == {
"count": 2,
"next": None,
"previous": None,
"results": [
{
"id": str(comment1.id),
"body": comment1.body,
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment1.user.full_name,
"short_name": comment1.user.short_name,
},
"abilities": comment1.get_abilities(AnonymousUser()),
"reactions": [],
},
{
"id": str(comment2.id),
"body": comment2.body,
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment2.user.full_name,
"short_name": comment2.user.short_name,
},
"abilities": comment2.get_abilities(AnonymousUser()),
"reactions": [],
},
],
}
@pytest.mark.parametrize("link_reach", ["restricted", "authenticated"])
def test_list_comments_anonymous_user_non_public_document(link_reach):
"""Anonymous users should not be allowed to list comments on a non-public document."""
document = factories.DocumentFactory(
link_reach=link_reach, link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
factories.CommentFactory(thread=thread)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
response = APIClient().get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
)
assert response.status_code == 401
def test_list_comments_authenticated_user_accessible_document():
"""Authenticated users should be allowed to list comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
comment1 = factories.CommentFactory(thread=thread)
comment2 = factories.CommentFactory(thread=thread, user=user)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
)
assert response.status_code == 200
assert response.json() == {
"count": 2,
"next": None,
"previous": None,
"results": [
{
"id": str(comment1.id),
"body": comment1.body,
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment1.user.full_name,
"short_name": comment1.user.short_name,
},
"abilities": comment1.get_abilities(user),
"reactions": [],
},
{
"id": str(comment2.id),
"body": comment2.body,
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment2.user.full_name,
"short_name": comment2.user.short_name,
},
"abilities": comment2.get_abilities(user),
"reactions": [],
},
],
}
def test_list_comments_authenticated_user_non_accessible_document():
"""Authenticated users should not be allowed to list comments on a non-accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
thread = factories.ThreadFactory(document=document)
factories.CommentFactory(thread=thread)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
)
assert response.status_code == 403
def test_list_comments_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to list comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
thread = factories.ThreadFactory(document=document)
factories.CommentFactory(thread=thread)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
)
assert response.status_code == 403
# Create comment
def test_create_comment_anonymous_user_public_document():
"""
Anonymous users should be allowed to create comments on a public document
with commenter link_role.
"""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
{"body": "test"},
)
assert response.status_code == 201
assert response.json() == {
"id": str(response.json()["id"]),
"body": "test",
"created_at": response.json()["created_at"],
"updated_at": response.json()["updated_at"],
"user": None,
"abilities": {
"destroy": False,
"update": False,
"partial_update": False,
"reactions": False,
"retrieve": True,
},
"reactions": [],
}
def test_create_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to create comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
{"body": "test"},
)
assert response.status_code == 401
def test_create_comment_authenticated_user_accessible_document():
"""Authenticated users should be allowed to create comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
{"body": "test"},
)
assert response.status_code == 201
assert response.json() == {
"id": str(response.json()["id"]),
"body": "test",
"created_at": response.json()["created_at"],
"updated_at": response.json()["updated_at"],
"user": {
"full_name": user.full_name,
"short_name": user.short_name,
},
"abilities": {
"destroy": True,
"update": True,
"partial_update": True,
"reactions": True,
"retrieve": True,
},
"reactions": [],
}
def test_create_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to create comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
thread = factories.ThreadFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
{"body": "test"},
)
assert response.status_code == 403
# Retrieve comment
def test_retrieve_comment_anonymous_user_public_document():
"""Anonymous users should be allowed to retrieve comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 200
assert response.json() == {
"id": str(comment.id),
"body": comment.body,
"created_at": comment.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment.user.full_name,
"short_name": comment.user.short_name,
},
"reactions": [],
"abilities": comment.get_abilities(AnonymousUser()),
}
def test_retrieve_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to retrieve comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_retrieve_comment_authenticated_user_accessible_document():
"""Authenticated users should be allowed to retrieve comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 200
def test_retrieve_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to retrieve comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
# Update comment
def test_update_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to update comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
client = APIClient()
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 401
def test_update_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to update comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
client = APIClient()
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 401
def test_update_comment_authenticated_user_accessible_document():
"""Authenticated users should not be able to update comments not their own."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted",
users=[
(
user,
random.choice(
[models.LinkRoleChoices.COMMENTER, models.LinkRoleChoices.EDITOR]
),
)
],
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 403
def test_update_comment_authenticated_user_own_comment():
"""Authenticated users should be able to update comments not their own."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted",
users=[
(
user,
random.choice(
[models.LinkRoleChoices.COMMENTER, models.LinkRoleChoices.EDITOR]
),
)
],
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test", user=user)
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.body == "other content"
def test_update_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to update comments on a document they don't
have comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 403
def test_update_comment_authenticated_no_access():
"""
Authenticated users should not be allowed to update comments on a document they don't
have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 403
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_any_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.body == "other content"
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_own_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test", user=user)
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.body == "other content"
# Delete comment
def test_delete_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to delete comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_delete_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to delete comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_delete_comment_authenticated_user_accessible_document_own_comment():
"""Authenticated users should be able to delete comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, user=user)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
def test_delete_comment_authenticated_user_accessible_document_not_own_comment():
"""Authenticated users should not be able to delete comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_any_comment(role):
"""Authenticated users should be able to delete comments on a document they have access to."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_own_comment(role):
"""Authenticated users should be able to delete comments on a document they have access to."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, user=user)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
def test_delete_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be able to delete comments on a document they don't
have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
# Create reaction
@pytest.mark.parametrize("link_role", models.LinkRoleChoices.values)
def test_create_reaction_anonymous_user_public_document(link_role):
"""No matter the link_role, an anonymous user can not react to a comment."""
document = factories.DocumentFactory(link_reach="public", link_role=link_role)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 401
def test_create_reaction_authenticated_user_public_document():
"""
Authenticated users should not be able to reaction to a comment on a public document with
link_role reader.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 403
def test_create_reaction_authenticated_user_accessible_public_document():
"""
Authenticated users should be able to react to a comment on a public document.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 201
assert models.Reaction.objects.filter(
comment=comment, emoji="test", users__in=[user]
).exists()
def test_create_reaction_authenticated_user_connected_document_link_role_reader():
"""
Authenticated users should not be able to react to a comment on a connected document
with link_role reader.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="authenticated", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 403
@pytest.mark.parametrize(
"link_role",
[
role
for role in models.LinkRoleChoices.values
if role != models.LinkRoleChoices.READER
],
)
def test_create_reaction_authenticated_user_connected_document(link_role):
"""
Authenticated users should be able to react to a comment on a connected document.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="authenticated", link_role=link_role
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 201
assert models.Reaction.objects.filter(
comment=comment, emoji="test", users__in=[user]
).exists()
def test_create_reaction_authenticated_user_restricted_accessible_document():
"""
Authenticated users should not be able to react to a comment on a restricted accessible document
they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 403
def test_create_reaction_authenticated_user_restricted_accessible_document_role_reader():
"""
Authenticated users should not be able to react to a comment on a restricted accessible
document with role reader.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", link_role=models.LinkRoleChoices.READER
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 403
@pytest.mark.parametrize(
"role",
[role for role in models.RoleChoices.values if role != models.RoleChoices.READER],
)
def test_create_reaction_authenticated_user_restricted_accessible_document_role_commenter(
role,
):
"""
Authenticated users should be able to react to a comment on a restricted accessible document
with role commenter.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted", users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 201
assert models.Reaction.objects.filter(
comment=comment, emoji="test", users__in=[user]
).exists()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 400
assert response.json() == {"user_already_reacted": True}
# Delete reaction
def test_delete_reaction_not_owned_by_the_current_user():
"""
Users should not be able to delete reactions not owned by the current user.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.RoleChoices.ADMIN)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
)
assert response.status_code == 404
def test_delete_reaction_owned_by_the_current_user():
"""
Users should not be able to delete reactions not owned by the current user.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.RoleChoices.ADMIN)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
)
assert response.status_code == 404
reaction.refresh_from_db()
assert reaction.users.exists()

View File

@@ -36,7 +36,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": document.link_role in ["commenter", "editor"],
"cors_proxy": True,
"content": True,
"descendants": True,
@@ -47,8 +46,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
@@ -114,7 +113,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": grand_parent.link_role in ["commenter", "editor"],
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -222,7 +220,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"children_create": document.link_role == "editor",
"children_list": True,
"collaboration_auth": True,
"comment": document.link_role in ["commenter", "editor"],
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -232,8 +229,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -307,7 +304,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"children_create": grand_parent.link_role == "editor",
"children_list": True,
"collaboration_auth": True,
"comment": grand_parent.link_role in ["commenter", "editor"],
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -498,14 +494,13 @@ def test_api_documents_retrieve_authenticated_related_parent():
"abilities": {
"accesses_manage": access.role in ["administrator", "owner"],
"accesses_view": True,
"ai_transform": access.role not in ["reader", "commenter"],
"ai_translate": access.role not in ["reader", "commenter"],
"attachment_upload": access.role not in ["reader", "commenter"],
"can_edit": access.role not in ["reader", "commenter"],
"children_create": access.role not in ["reader", "commenter"],
"ai_transform": access.role != "reader",
"ai_translate": access.role != "reader",
"attachment_upload": access.role != "reader",
"can_edit": access.role != "reader",
"children_create": access.role != "reader",
"children_list": True,
"collaboration_auth": True,
"comment": access.role != "reader",
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -521,11 +516,11 @@ def test_api_documents_retrieve_authenticated_related_parent():
"media_auth": True,
"media_check": True,
"move": access.role in ["administrator", "owner"],
"partial_update": access.role not in ["reader", "commenter"],
"partial_update": access.role != "reader",
"restore": access.role == "owner",
"retrieve": True,
"tree": True,
"update": access.role not in ["reader", "commenter"],
"update": access.role != "reader",
"versions_destroy": access.role in ["administrator", "owner"],
"versions_list": True,
"versions_retrieve": True,

File diff suppressed because it is too large Load Diff

View File

@@ -81,7 +81,6 @@ def test_api_documents_trashbin_format():
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"comment": False,
"content": False,
"destroy": False,
"duplicate": False,
@@ -89,8 +88,8 @@ def test_api_documents_trashbin_format():
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,

View File

@@ -278,35 +278,6 @@ def test_api_users_retrieve_me_authenticated():
}
def test_api_users_retrieve_me_authenticated_empty_name():
"""
Authenticated users should be able to retrieve their own user via the "/users/me" path.
when no name is provided, the full name and short name should be the email without the domain.
"""
user = factories.UserFactory(
email="test_foo@test.com",
full_name=None,
short_name=None,
)
client = APIClient()
client.force_login(user)
factories.UserFactory.create_batch(2)
response = client.get(
"/api/v1.0/users/me/",
)
assert response.status_code == 200
assert response.json() == {
"id": str(user.id),
"email": "test_foo@test.com",
"full_name": "test_foo",
"language": user.language,
"short_name": "test_foo",
}
def test_api_users_retrieve_anonymous():
"""Anonymous users should not be allowed to retrieve a user."""
client = APIClient()

View File

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

View File

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

View File

@@ -134,13 +134,10 @@ def test_models_documents_soft_delete(depth):
[
(True, "restricted", "reader"),
(True, "restricted", "editor"),
(True, "restricted", "commenter"),
(False, "restricted", "reader"),
(False, "restricted", "editor"),
(False, "restricted", "commenter"),
(False, "authenticated", "reader"),
(False, "authenticated", "editor"),
(False, "authenticated", "commenter"),
],
)
def test_models_documents_get_abilities_forbidden(
@@ -168,7 +165,6 @@ def test_models_documents_get_abilities_forbidden(
"destroy": False,
"duplicate": False,
"favorite": False,
"comment": False,
"invite_owner": False,
"mask": False,
"media_auth": False,
@@ -176,8 +172,8 @@ def test_models_documents_get_abilities_forbidden(
"move": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"partial_update": False,
@@ -227,7 +223,6 @@ def test_models_documents_get_abilities_reader(
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": False,
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -237,78 +232,8 @@ def test_models_documents_get_abilities_reader(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,
"retrieve": True,
"tree": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
}
nb_queries = 1 if is_authenticated else 0
with django_assert_num_queries(nb_queries):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(
value is False
for key, value in document.get_abilities(user).items()
if key not in ["link_select_options", "ancestors_links_definition"]
)
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_documents_get_abilities_commenter(
is_authenticated, reach, django_assert_num_queries
):
"""
Check abilities returned for a document giving commenter 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="commenter")
user = factories.UserFactory() if is_authenticated else AnonymousUser()
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"content": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": is_authenticated,
@@ -364,7 +289,6 @@ def test_models_documents_get_abilities_editor(
"children_create": is_authenticated,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -374,8 +298,8 @@ def test_models_documents_get_abilities_editor(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": is_authenticated,
@@ -420,7 +344,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -430,8 +353,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"invite_owner": True,
"link_configuration": True,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -462,7 +385,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"children_create": False,
"children_list": False,
"collaboration_auth": False,
"comment": False,
"descendants": False,
"cors_proxy": False,
"content": False,
@@ -472,8 +394,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
@@ -508,7 +430,6 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -518,8 +439,8 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"invite_owner": False,
"link_configuration": True,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -564,7 +485,6 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -574,8 +494,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -627,8 +547,6 @@ def test_models_documents_get_abilities_reader_user(
"children_create": access_from_link,
"children_list": True,
"collaboration_auth": True,
"comment": document.link_reach != "restricted"
and document.link_role in ["commenter", "editor"],
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -638,73 +556,8 @@ def test_models_documents_get_abilities_reader_user(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": access_from_link,
"restore": False,
"retrieve": True,
"tree": True,
"update": access_from_link,
"versions_destroy": False,
"versions_list": True,
"versions_retrieve": True,
}
with override_settings(AI_ALLOW_REACH_FROM=ai_access_setting):
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(
value is False
for key, value in document.get_abilities(user).items()
if key not in ["link_select_options", "ancestors_links_definition"]
)
@pytest.mark.parametrize("ai_access_setting", ["public", "authenticated", "restricted"])
def test_models_documents_get_abilities_commenter_user(
ai_access_setting, django_assert_num_queries
):
"""Check abilities returned for the commenter of a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "commenter")])
access_from_link = (
document.link_reach != "restricted" and document.link_role == "editor"
)
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
"can_edit": access_from_link,
"children_create": access_from_link,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"content": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": True,
"favorite": True,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -754,7 +607,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": False,
"descendants": True,
"cors_proxy": True,
"content": True,
@@ -764,8 +616,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -1468,14 +1320,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"public",
"reader",
{
"public": ["reader", "commenter", "editor"],
},
),
(
"public",
"commenter",
{
"public": ["commenter", "editor"],
"public": ["reader", "editor"],
},
),
("public", "editor", {"public": ["editor"]}),
@@ -1483,16 +1328,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"authenticated",
"reader",
{
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
},
),
(
"authenticated",
"commenter",
{
"authenticated": ["commenter", "editor"],
"public": ["commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
},
),
(
@@ -1505,17 +1342,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"reader",
{
"restricted": None,
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "commenter", "editor"],
},
),
(
"restricted",
"commenter",
{
"restricted": None,
"authenticated": ["commenter", "editor"],
"public": ["commenter", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
},
),
(
@@ -1532,15 +1360,15 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"public",
None,
{
"public": ["reader", "commenter", "editor"],
"public": ["reader", "editor"],
},
),
(
None,
"reader",
{
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "editor"],
"authenticated": ["reader", "editor"],
"restricted": None,
},
),
@@ -1548,8 +1376,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
None,
None,
{
"public": ["reader", "commenter", "editor"],
"authenticated": ["reader", "commenter", "editor"],
"public": ["reader", "editor"],
"authenticated": ["reader", "editor"],
"restricted": None,
},
),

View File

@@ -26,24 +26,13 @@ document_related_router.register(
viewsets.InvitationViewset,
basename="invitations",
)
document_related_router.register(
"threads",
viewsets.ThreadViewSet,
basename="threads",
)
document_related_router.register(
"ask-for-access",
viewsets.DocumentAskForAccessViewSet,
basename="ask_for_access",
)
thread_related_router = DefaultRouter()
thread_related_router.register(
"comments",
viewsets.CommentViewSet,
basename="comments",
)
urlpatterns = [
path(
@@ -56,10 +45,6 @@ urlpatterns = [
r"^documents/(?P<resource_id>[0-9a-z-]*)/",
include(document_related_router.urls),
),
re_path(
r"^documents/(?P<resource_id>[0-9a-z-]*)/threads/(?P<thread_id>[0-9a-z-]*)/",
include(thread_related_router.urls),
),
]
),
),

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -234,8 +234,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "titl"
@@ -311,8 +311,8 @@ msgstr "An implijer-mañ a zo dija er restr-mañ."
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
@@ -328,78 +328,78 @@ msgstr "Goulennoù tizhout ar restr"
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -234,8 +234,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "Titel"
@@ -311,8 +311,8 @@ msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -234,8 +234,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "título"
@@ -311,8 +311,8 @@ msgstr "Este usuario ya forma parte del documento."
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
@@ -328,78 +328,78 @@ msgstr "Solicitud de accesos"
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -234,8 +234,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "titre"
@@ -311,8 +311,8 @@ msgstr "Cet utilisateur est déjà dans ce document."
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
@@ -328,78 +328,78 @@ msgstr "Demande d'accès au document"
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -234,8 +234,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "titolo"
@@ -311,8 +311,8 @@ msgstr "Questo utente è già presente in questo documento."
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -234,8 +234,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "titel"
@@ -311,8 +311,8 @@ msgstr "De gebruiker bestaat al in dit document."
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
@@ -328,78 +328,78 @@ msgstr "Document verzoekt om toegangen"
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Sjabloon"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Sjabloon"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Sjabloon/gebruiker relatie"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Sjabloon/gebruiker relaties"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit sjabloon."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit sjabloon."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -234,8 +234,8 @@ msgstr "пользователь"
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "заголовок"
@@ -311,8 +311,8 @@ msgstr "Этот пользователь уже имеет доступ к эт
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
@@ -328,78 +328,78 @@ msgstr "Документ запрашивает доступы"
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -234,8 +234,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "naslov"
@@ -311,8 +311,8 @@ msgstr "Ta uporabnik je že v tem dokumentu."
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -234,8 +234,8 @@ msgstr "користувач"
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "заголовок"
@@ -311,8 +311,8 @@ msgstr "Цей користувач вже має доступ до цього
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
@@ -328,78 +328,78 @@ msgstr "Запит доступу для документа"
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -234,8 +234,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
msgid "title"
msgstr "标题"
@@ -311,8 +311,8 @@ msgstr "该用户已在此文档中。"
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
@@ -328,78 +328,78 @@ msgstr "文档需要访问权限"
msgid "This user has already asked for access to this document."
msgstr "用户已申请该文档的访问权限。"
#: build/lib/core/models.py:1255 core/models.py:1255
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 申请访问文档!"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1267 core/models.py:1267
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 申请访问以下文档:"
#: build/lib/core/models.py:1265 core/models.py:1265
#: build/lib/core/models.py:1273 core/models.py:1273
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name}申请文档:{title}的访问权限"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1291 core/models.py:1291
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1343 core/models.py:1343
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1344 core/models.py:1344
#: build/lib/core/models.py:1352 core/models.py:1352
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1358 core/models.py:1358
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1364 core/models.py:1364
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1452 core/models.py:1452
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1453 core/models.py:1453
#: build/lib/core/models.py:1461 core/models.py:1461
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1473 core/models.py:1473
#: build/lib/core/models.py:1481 core/models.py:1481
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.10.0"
version = "3.9.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -47,15 +47,6 @@ ENV NEXT_PUBLIC_SW_DEACTIVATED=${SW_DEACTIVATED}
ARG PUBLISH_AS_MIT
ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
ARG CUSTOM_CODE
COPY ./${CUSTOM_CODE} /tmp/custom_code
RUN if [ -n "$CUSTOM_CODE" ] && [ -d "/tmp/custom_code" ] && [ "$(ls -A /tmp/custom_code)" ]; then \
echo "Custom code provided. Replacing files from $CUSTOM_CODE..."; \
cp -Rv /tmp/custom_code/${CUSTOM_CODE}/* .; \
else \
echo "No custom code provided. Skipping replacement..."; \
fi
RUN yarn build
# ---- Front-end image ----

View File

@@ -1,295 +0,0 @@
import { expect, test } from '@playwright/test';
import { createDoc, getOtherBrowserName, verifyDocName } from './utils-common';
import { writeInEditor } from './utils-editor';
import {
addNewMember,
connectOtherUserToDoc,
updateRoleUser,
updateShareLink,
} from './utils-share';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Comments', () => {
test('it checks comments with 2 users in real time', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
// We share the doc with another user
const otherBrowserName = getOtherBrowserName(browserName);
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Administrator', otherBrowserName);
await expect(
page
.getByRole('listbox', { name: 'Suggestions' })
.getByText(new RegExp(otherBrowserName)),
).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
// We add a comment with the first user
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
await editor.getByText('Hello').click();
await thread.getByText('This is a comment').first().hover();
// We add a reaction with the first user
await thread.locator('[data-test="addreaction"]').first().click();
await page.getByRole('button', { name: '👍' }).click();
await expect(
thread.getByRole('img', { name: 'E2E Chromium' }).first(),
).toBeVisible();
await expect(thread.getByText('This is a comment').first()).toBeVisible();
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
const urlCommentDoc = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
otherBrowserName,
docUrl: urlCommentDoc,
docTitle,
});
const otherEditor = otherPage.locator('.ProseMirror');
await otherEditor.getByText('Hello').click();
const otherThread = otherPage.locator('.bn-thread');
await otherThread.getByText('This is a comment').first().hover();
await otherThread.locator('[data-test="addreaction"]').first().click();
await otherPage.getByRole('button', { name: '👍' }).click();
// We check that the comment made by the first user is visible for the second user
await expect(
otherThread.getByText('This is a comment').first(),
).toBeVisible();
await expect(
otherThread.getByText(`E2E ${browserName}`).first(),
).toBeVisible();
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
// We add a comment with the second user
await otherThread
.getByRole('paragraph')
.last()
.fill('This is a comment from the other user');
await otherThread.locator('[data-test="save"]').click();
// We check that the second user can see the comment he just made
await expect(
otherThread.getByRole('img', { name: `E2E ${otherBrowserName}` }).first(),
).toBeVisible();
await expect(
otherThread.getByText('This is a comment from the other user').first(),
).toBeVisible();
await expect(
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
).toBeVisible();
// We check that the first user can see the comment made by the second user in real time
await expect(
thread.getByText('This is a comment from the other user').first(),
).toBeVisible();
await expect(
thread.getByText(`E2E ${otherBrowserName}`).first(),
).toBeVisible();
await cleanup();
});
test('it checks the comments interactions', async ({ page, browserName }) => {
await createDoc(page, 'comment-interaction', browserName, 1);
// Checks add react reaction
const editor = page.locator('.ProseMirror');
await editor.locator('.bn-block-outer').last().fill('Hello World');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
// Check background color changed
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await editor.getByText('Hello').click();
await thread.getByText('This is a comment').first().hover();
// We add a reaction with the first user
await thread.locator('[data-test="addreaction"]').first().click();
await page.getByRole('button', { name: '👍' }).click();
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
// Edit Comment
await thread.getByText('This is a comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
const commentEditor = thread.getByText('This is a comment').first();
await commentEditor.fill('This is an edited comment');
const saveBtn = thread.getByRole('button', { name: 'Save' });
await saveBtn.click();
await expect(saveBtn).toBeHidden();
await expect(
thread.getByText('This is an edited comment').first(),
).toBeVisible();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
// Add second comment
await thread.getByRole('paragraph').last().fill('This is a second comment');
await thread.getByRole('button', { name: 'Save' }).click();
await expect(
thread.getByText('This is an edited comment').first(),
).toBeVisible();
await expect(
thread.getByText('This is a second comment').first(),
).toBeVisible();
// Delete second comment
await thread.getByText('This is a second comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await expect(
thread.getByText('This is a second comment').first(),
).toBeHidden();
// Resolve thread
await thread.getByText('This is an edited comment').first().hover();
await thread.locator('[data-test="resolve"]').click();
await expect(thread).toBeHidden();
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
});
test('it checks the comments abilities', async ({ page, browserName }) => {
test.slow();
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
// We share the doc with another user
const otherBrowserName = getOtherBrowserName(browserName);
// Add a new member with editor role
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor', otherBrowserName);
await expect(
page
.getByRole('listbox', { name: 'Suggestions' })
.getByText(new RegExp(otherBrowserName)),
).toBeVisible();
const urlCommentDoc = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
otherBrowserName,
docUrl: urlCommentDoc,
docTitle,
});
const otherEditor = await writeInEditor({
page: otherPage,
text: 'Hello, I can edit the document',
});
await expect(
otherEditor.getByText('Hello, I can edit the document'),
).toBeVisible();
await otherEditor.getByText('Hello').selectText();
await otherPage.getByRole('button', { name: 'Comment' }).click();
const otherThread = otherPage.locator('.bn-thread');
await otherThread
.getByRole('paragraph')
.first()
.fill('I can add a comment');
await otherThread.locator('[data-test="save"]').click();
await expect(
otherThread.getByText('I can add a comment').first(),
).toBeHidden();
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
// We change the role of the second user to reader
await updateRoleUser(page, 'Reader', `user.test@${otherBrowserName}.test`);
// With the reader role, the second user cannot see comments
await otherPage.reload();
await verifyDocName(otherPage, docTitle);
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await otherEditor.getByText('Hello').click();
await expect(otherThread).toBeHidden();
await otherEditor.getByText('Hello').selectText();
await expect(
otherPage.getByRole('button', { name: 'Comment' }),
).toBeHidden();
await otherPage.reload();
// Change the link role of the doc to set it in commenting mode
await updateShareLink(page, 'Public', 'Editing');
// Anonymous user can see and add comments
await otherPage.getByRole('button', { name: 'Logout' }).click();
await otherPage.goto(urlCommentDoc);
await verifyDocName(otherPage, docTitle);
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await otherEditor.getByText('Hello').click();
await expect(
otherThread.getByText('I can add a comment').first(),
).toBeVisible();
await otherThread
.locator('.ProseMirror.bn-editor[contenteditable="true"]')
.getByRole('paragraph')
.first()
.fill('Comment by anonymous user');
await otherThread.locator('[data-test="save"]').click();
await expect(
otherThread.getByText('Comment by anonymous user').first(),
).toBeVisible();
await expect(
otherThread.getByRole('img', { name: `Anonymous` }).first(),
).toBeVisible();
await otherThread.getByText('Comment by anonymous user').first().hover();
await expect(otherThread.locator('[data-test="moreactions"]')).toBeHidden();
await cleanup();
});
});

View File

@@ -84,7 +84,7 @@ test.describe('Document create member', () => {
// Validate
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByRole('button', { name: /^Invite / }).click();
await page.getByRole('button', { name: 'Invite' }).click();
// Check invitation added
await expect(
@@ -135,7 +135,7 @@ test.describe('Document create member', () => {
(response) =>
response.url().includes('/invitations/') && response.status() === 201,
);
await page.getByRole('button', { name: /^Invite / }).click();
await page.getByRole('button', { name: 'Invite' }).click();
// Check invitation sent
@@ -154,7 +154,7 @@ test.describe('Document create member', () => {
response.url().includes('/invitations/') && response.status() === 400,
);
await page.getByRole('button', { name: /^Invite / }).click();
await page.getByRole('button', { name: 'Invite' }).click();
await expect(
page.getByText(`"${email}" is already invited to the document.`),
).toBeVisible();
@@ -191,7 +191,7 @@ test.describe('Document create member', () => {
response.url().includes('/invitations/') && response.status() === 201,
);
await page.getByRole('button', { name: /^Invite / }).click();
await page.getByRole('button', { name: 'Invite' }).click();
// Check invitation sent
const responseCreateInvitation = await responsePromiseCreateInvitation;

View File

@@ -70,14 +70,6 @@ export const keyCloakSignIn = async (
await page.click('button[type="submit"]', { force: true });
};
export const getOtherBrowserName = (browserName: BrowserName) => {
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
if (!otherBrowserName) {
throw new Error('No alternative browser found');
}
return otherBrowserName;
};
export const randomName = (name: string, browserName: string, length: number) =>
Array.from({ length }, (_el, index) => {
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
@@ -133,9 +125,7 @@ export const verifyDocName = async (page: Page, docName: string) => {
try {
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(docName, {
timeout: 1000,
});
).toContainText(docName);
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}

View File

@@ -1,8 +1,8 @@
import { Page, chromium, expect } from '@playwright/test';
import {
BROWSERS,
BrowserName,
getOtherBrowserName,
keyCloakSignIn,
verifyDocName,
} from './utils-common';
@@ -40,7 +40,7 @@ export const addNewMember = async (
// Choose a role
await page.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: role }).click();
await page.getByRole('button', { name: /^Invite / }).click();
await page.getByRole('button', { name: 'Invite' }).click();
return users[index].email;
};
@@ -88,30 +88,21 @@ export const updateRoleUser = async (
* @param docTitle The title of the document (optional).
* @returns An object containing the other browser, context, and page.
*/
type ConnectOtherUserToDocParams = {
docUrl: string;
docTitle?: string;
withoutSignIn?: boolean;
} & (
| {
otherBrowserName: BrowserName;
browserName?: never;
}
| {
browserName: BrowserName;
otherBrowserName?: never;
}
);
export const connectOtherUserToDoc = async ({
browserName,
docUrl,
docTitle,
otherBrowserName: _otherBrowserName,
withoutSignIn,
}: ConnectOtherUserToDocParams) => {
const otherBrowserName =
_otherBrowserName || getOtherBrowserName(browserName);
}: {
browserName: BrowserName;
docUrl: string;
docTitle?: string;
withoutSignIn?: boolean;
}) => {
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
if (!otherBrowserName) {
throw new Error('No alternative browser found');
}
const otherBrowser = await chromium.launch({ headless: true });
const otherContext = await otherBrowser.newContext({

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.10.0",
"version": "3.9.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",

View File

@@ -98,8 +98,8 @@ const dsfrTheme = {
},
font: {
families: {
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
base: 'Marianne',
accent: 'Marianne',
},
},
},

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.10.0",
"version": "3.9.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -19,14 +19,14 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.42.3",
"@blocknote/core": "0.42.3",
"@blocknote/mantine": "0.42.3",
"@blocknote/react": "0.42.3",
"@blocknote/xl-docx-exporter": "0.42.3",
"@blocknote/xl-multi-column": "0.42.3",
"@blocknote/xl-odt-exporter": "0.42.3",
"@blocknote/xl-pdf-exporter": "0.42.3",
"@blocknote/code-block": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@2183",
"@blocknote/core": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2183",
"@blocknote/mantine": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@2183",
"@blocknote/react": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@2183",
"@blocknote/xl-docx-exporter": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@2183",
"@blocknote/xl-multi-column": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@2183",
"@blocknote/xl-odt-exporter": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@2183",
"@blocknote/xl-pdf-exporter": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@2183",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",

View File

@@ -556,10 +556,8 @@
--c--theme--logo--widthHeader: 110px;
--c--theme--logo--widthFooter: 220px;
--c--theme--logo--alt: gouvernement logo;
--c--theme--font--families--base:
marianne, inter, roboto flex variable, sans-serif;
--c--theme--font--families--accent:
marianne, inter, roboto flex variable, sans-serif;
--c--theme--font--families--base: marianne;
--c--theme--font--families--accent: marianne;
--c--components--la-gaufre: true;
--c--components--home-proconnect: true;
--c--components--favicon--ico: /assets/favicon-dsfr.ico;

View File

@@ -436,12 +436,7 @@ export const tokens = {
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
font: {
families: {
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
},
},
font: { families: { base: 'Marianne', accent: 'Marianne' } },
},
components: {
'la-gaufre': true,

View File

@@ -13,5 +13,3 @@ export interface User {
short_name: string;
language?: string;
}
export type UserLight = Pick<User, 'full_name' | 'short_name'>;

View File

@@ -1,49 +0,0 @@
import React from 'react';
import { Box, BoxType } from '@/components';
type AvatarSvgProps = {
initials: string;
background: string;
fontFamily?: string;
} & BoxType;
export const AvatarSvg: React.FC<AvatarSvgProps> = ({
initials,
background,
fontFamily,
...props
}) => (
<Box
as="svg"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<rect
x="0.5"
y="0.5"
width="23"
height="23"
rx="11.5"
ry="11.5"
fill={background}
stroke="rgba(255,255,255,0.5)"
strokeWidth="1"
/>
<text
x="50%"
y="50%"
dy="0.35em"
textAnchor="middle"
fontSize="10"
fontWeight="600"
fill="rgba(255,255,255,0.9)"
fontFamily={fontFamily || 'Arial'}
>
{initials}
</text>
</Box>
);

View File

@@ -1,70 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { tokens } from '@/cunningham';
import { AvatarSvg } from './AvatarSvg';
const colors = tokens.themes.default.theme.colors;
const avatarsColors = [
colors['blue-500'],
colors['brown-500'],
colors['cyan-500'],
colors['gold-500'],
colors['green-500'],
colors['olive-500'],
colors['orange-500'],
colors['pink-500'],
colors['purple-500'],
colors['yellow-500'],
];
const getColorFromName = (name: string) => {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return avatarsColors[Math.abs(hash) % avatarsColors.length];
};
const getInitialFromName = (name: string) => {
const splitName = name?.split(' ');
return (splitName[0]?.charAt(0) || '?') + (splitName?.[1]?.charAt(0) || '');
};
type UserAvatarProps = {
fullName?: string;
background?: string;
};
export const UserAvatar = ({ fullName, background }: UserAvatarProps) => {
const name = fullName?.trim() || '?';
return (
<AvatarSvg
className="--docs--user-avatar"
initials={getInitialFromName(name).toUpperCase()}
background={background || getColorFromName(name)}
/>
);
};
export const avatarUrlFromName = (
fullName?: string,
fontFamily?: string,
): string => {
const name = fullName?.trim() || '?';
const initials = getInitialFromName(name).toUpperCase();
const background = getColorFromName(name);
const svgMarkup = renderToStaticMarkup(
<AvatarSvg
className="--docs--user-avatar"
initials={initials}
background={background}
fontFamily={fontFamily}
/>,
);
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgMarkup)}`;
};

View File

@@ -1,3 +1,2 @@
export * from './Auth';
export * from './ButtonLogin';
export * from './UserAvatar';

View File

@@ -12,16 +12,13 @@ import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, useProviderStore } from '@/docs/doc-management';
import { avatarUrlFromName, useAuth } from '@/features/auth';
import { useResponsiveStore } from '@/stores';
import { useAuth } from '@/features/auth';
import {
useHeadings,
@@ -37,7 +34,6 @@ import { randomColor } from '../utils';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { cssComments, useComments } from './comments/';
import {
AccessibleImageBlock,
CalloutBlock,
@@ -83,11 +79,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { user } = useAuth();
const { setEditor } = useEditorStore();
const { t } = useTranslation();
const { themeTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { isSynced: isConnectedToCollabServer } = useProviderStore();
const refEditorContainer = useRef<HTMLDivElement>(null);
const canSeeComment = doc.abilities.comment && isDesktop;
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
const { i18n } = useTranslation();
@@ -95,25 +88,16 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
const collabName = user?.full_name || user?.email;
const cursorName = collabName || t('Anonymous');
const collabName = user?.full_name || user?.email || t('Anonymous');
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
const threadStore = useComments(doc.id, canSeeComment, user);
const currentUserAvatarUrl = useMemo(() => {
if (canSeeComment) {
return avatarUrlFromName(collabName, themeTokens?.font?.families?.base);
}
}, [canSeeComment, collabName, themeTokens?.font?.families?.base]);
const editor: DocsBlockNoteEditor = useCreateBlockNote(
{
collaboration: {
provider: provider,
fragment: provider.document.getXmlFragment('document-store'),
user: {
name: cursorName,
name: collabName,
color: randomColor(),
},
/**
@@ -154,28 +138,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
},
showCursorLabels: showCursorLabels as 'always' | 'activity',
},
comments: { threadStore },
dictionary: {
...locales[lang as keyof typeof locales],
multi_column:
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
},
resolveUsers: async (userIds) => {
return Promise.resolve(
userIds.map((encodedURIUserId) => {
const fullName = decodeURIComponent(encodedURIUserId);
return {
id: encodedURIUserId,
username: fullName || t('Anonymous'),
avatarUrl: avatarUrlFromName(
fullName,
themeTokens?.font?.families?.base,
),
};
}),
);
},
tables: {
splitCells: true,
cellBackgroundColor: true,
@@ -185,7 +152,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
uploadFile,
schema: blockNoteSchema,
},
[cursorName, lang, provider, uploadFile, threadStore],
[collabName, lang, provider, uploadFile],
);
useHeadings(editor);
@@ -203,13 +170,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
}, [setEditor, editor]);
return (
<Box
ref={refEditorContainer}
$css={css`
${cssEditor};
${cssComments(canSeeComment, currentUserAvatarUrl)}
`}
>
<Box ref={refEditorContainer} $css={cssEditor}>
{errorAttachment && (
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
<TextErrors
@@ -219,13 +180,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
/>
</Box>
)}
<BlockNoteView
className="--docs--main-editor"
editor={editor}
formattingToolbar={false}
slashMenu={false}
theme="light"
comments={canSeeComment}
aria-label={t('Document editor')}
>
<BlockNoteSuggestionMenu />
@@ -236,17 +196,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
};
interface BlockNoteReaderProps {
docId: Doc['id'];
initialContent: Y.XmlFragment;
}
export const BlockNoteReader = ({
docId,
initialContent,
}: BlockNoteReaderProps) => {
const { user } = useAuth();
export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => {
const { setEditor } = useEditorStore();
const threadStore = useComments(docId, false, user);
const { t } = useTranslation();
const editor = useCreateBlockNote(
{
@@ -259,10 +213,6 @@ export const BlockNoteReader = ({
provider: undefined,
},
schema: blockNoteSchema,
comments: { threadStore },
resolveUsers: async () => {
return Promise.resolve([]);
},
},
[initialContent],
);
@@ -278,21 +228,14 @@ export const BlockNoteReader = ({
useHeadings(editor);
return (
<Box
$css={css`
${cssEditor};
${cssComments(false)}
`}
>
<Box $css={cssEditor}>
<BlockNoteView
className="--docs--main-editor"
editor={editor}
editable={false}
theme="light"
aria-label={t('Document version viewer')}
formattingToolbar={false}
slashMenu={false}
comments={false}
/>
</Box>
);

View File

@@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next';
import { useConfig } from '@/core/config/api';
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
import { AIGroupButton } from './AIButton';
@@ -26,12 +25,10 @@ export const BlockNoteToolbar = () => {
const { data: conf } = useConfig();
const toolbarItems = useMemo(() => {
let toolbarItems = getFormattingToolbarItems([
const toolbarItems = getFormattingToolbarItems([
...blockTypeSelectItems(dict),
getCalloutFormattingToolbarItems(t),
]);
// Find the index of the file download button
const fileDownloadButtonIndex = toolbarItems.findIndex(
(item) =>
typeof item === 'object' &&
@@ -39,8 +36,6 @@ export const BlockNoteToolbar = () => {
'key' in item &&
(item as { key: string }).key === 'fileDownloadButton',
);
// Replace the default file download button with our custom FileDownloadButton
if (fileDownloadButtonIndex !== -1) {
toolbarItems.splice(
fileDownloadButtonIndex,
@@ -55,22 +50,12 @@ export const BlockNoteToolbar = () => {
);
}
// Remove default Comment button
toolbarItems = toolbarItems.filter((item) => {
if (typeof item === 'object' && item !== null && 'key' in item) {
return item.key !== 'addCommentButton';
}
return true;
});
return toolbarItems;
}, [dict, t]);
const formattingToolbar = useCallback(() => {
return (
<FormattingToolbar>
<CommentToolbarButton />
{toolbarItems}
{/* Extra button to do some AI powered actions */}

View File

@@ -6,9 +6,6 @@ import {
import { forEach, isArray } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Text } from '@/components';
type Block = {
type: string;
@@ -86,18 +83,8 @@ export function MarkdownButton() {
mainTooltip={t('Convert Markdown')}
onClick={handleConvertMarkdown}
className="--docs--editor-markdown-button"
label="M"
icon={
<Text
aria-hidden={true}
$css={css`
font-family: var(--c--theme--font--families--base);
`}
$weight="bold"
>
M
</Text>
}
/>
>
M
</Components.FormattingToolbar.Button>
);
}

View File

@@ -117,7 +117,6 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
initialContent={provider.document.getXmlFragment(
'document-store',
)}
docId={doc.id}
/>
) : (
<BlockNoteEditor doc={doc} provider={provider} />

View File

@@ -1,69 +0,0 @@
import { useBlockNoteEditor, useComponentsContext } from '@blocknote/react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useDocStore } from '@/features/docs/doc-management';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
export const CommentToolbarButton = () => {
const Components = useComponentsContext();
const { currentDoc } = useDocStore();
const { t } = useTranslation();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
if (!editor.isEditable || !Components || !currentDoc?.abilities.comment) {
return null;
}
return (
<Box $direction="row" className="--docs--comment-toolbar-button">
<Components.Generic.Toolbar.Button
className="bn-button"
onClick={() => {
editor.comments?.startPendingComment();
}}
aria-haspopup="dialog"
>
<Box
$direction="row"
$align="center"
$gap={spacingsTokens['xs']}
$padding={{ right: '2xs' }}
>
<Icon
iconName="comment"
className="--docs--icon-bg"
$theme="greyscale"
$variation="600"
$padding="0.15rem"
$size="16px"
$color={colorsTokens['greyscale-600']}
/>
{t('Comment')}
</Box>
</Components.Generic.Toolbar.Button>
<Box
$background={colorsTokens['greyscale-100']}
$width="1px"
$height="70%"
$margin={{ left: '2px' }}
$css={css`
align-self: center;
`}
/>
</Box>
);
};

View File

@@ -1,569 +0,0 @@
import { CommentBody, ThreadStore } from '@blocknote/core/comments';
import type { Awareness } from 'y-protocols/awareness';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs/doc-management';
import { useEditorStore } from '../../stores';
import { DocsThreadStoreAuth } from './DocsThreadStoreAuth';
import {
ClientCommentData,
ClientThreadData,
ServerComment,
ServerReaction,
ServerThread,
} from './types';
type ServerThreadListResponse = APIList<ServerThread>;
export class DocsThreadStore extends ThreadStore {
protected static COMMENTS_PING = 'commentsPing';
protected threads: Map<string, ClientThreadData> = new Map();
private subscribers = new Set<
(threads: Map<string, ClientThreadData>) => void
>();
private awareness?: Awareness;
private lastPingAt = 0;
private pingTimer?: ReturnType<typeof setTimeout>;
constructor(
protected docId: Doc['id'],
awareness: Awareness | undefined,
protected docAuth: DocsThreadStoreAuth,
) {
super(docAuth);
if (docAuth.canSee) {
this.awareness = awareness;
this.awareness?.on('update', this.onAwarenessUpdate);
void this.refreshThreads();
}
}
public destroy() {
this.awareness?.off('update', this.onAwarenessUpdate);
if (this.pingTimer) {
clearTimeout(this.pingTimer);
}
}
private onAwarenessUpdate = async ({
added,
updated,
}: {
added: number[];
updated: number[];
}) => {
if (!this.awareness) {
return;
}
const states = this.awareness.getStates();
const listClientIds = [...added, ...updated];
for (const clientId of listClientIds) {
// Skip our own client ID
if (clientId === this.awareness.clientID) {
continue;
}
const state = states.get(clientId) as
| {
[DocsThreadStore.COMMENTS_PING]?: {
at: number;
docId: string;
isResolving: boolean;
threadId: string;
};
}
| undefined;
const ping = state?.commentsPing;
// Skip if no ping information is available
if (!ping) {
continue;
}
// Skip if the document ID doesn't match
if (ping.docId !== this.docId) {
continue;
}
// Skip if the ping timestamp is past
if (ping.at <= this.lastPingAt) {
continue;
}
this.lastPingAt = ping.at;
// If we know the threadId, schedule a targeted refresh. Otherwise, fall back to full refresh.
if (ping.threadId) {
await this.refreshThread(ping.threadId);
} else {
await this.refreshThreads();
}
}
};
/**
* To ping the other clients for updates on a specific thread
* @param threadId
*/
private ping(threadId?: string) {
this.awareness?.setLocalStateField(DocsThreadStore.COMMENTS_PING, {
at: Date.now(),
docId: this.docId,
threadId,
});
}
/**
* Notifies all subscribers about the current thread state
*/
private notifySubscribers() {
// Always emit a new Map reference to help consumers detect changes
const threads = new Map(this.threads);
this.subscribers.forEach((cb) => {
try {
cb(threads);
} catch (e) {
console.warn('DocsThreadStore subscriber threw', e);
}
});
}
private upsertClientThreadData(thread: ClientThreadData) {
const next = new Map(this.threads);
next.set(thread.id, thread);
this.threads = next;
}
private removeThread(threadId: string) {
const next = new Map(this.threads);
next.delete(threadId);
this.threads = next;
}
/**
* To subscribe to thread updates
* @param cb
* @returns
*/
public subscribe(cb: (threads: Map<string, ClientThreadData>) => void) {
if (!this.docAuth.canSee) {
return () => {};
}
this.subscribers.add(cb);
// Emit initial state asynchronously to avoid running during editor init
setTimeout(() => {
if (this.subscribers.has(cb)) {
cb(this.getThreads());
}
}, 0);
return () => {
this.subscribers.delete(cb);
};
}
public addThreadToDocument = (options: {
threadId: string;
selection: {
prosemirror: {
head: number;
anchor: number;
};
yjs: {
head: unknown;
anchor: unknown;
};
};
}) => {
const { threadId } = options;
const { editor } = useEditorStore.getState();
// Should not happen
if (!editor) {
console.warn('Editor to add thread not ready');
return Promise.resolve();
}
editor._tiptapEditor
.chain()
.focus?.()
.setMark?.('comment', { orphan: false, threadId })
.run?.();
return Promise.resolve();
};
public createThread = async (options: {
initialComment: {
body: CommentBody;
metadata?: unknown;
};
metadata?: unknown;
}) => {
const response = await fetchAPI(`documents/${this.docId}/threads/`, {
method: 'POST',
body: JSON.stringify({
body: options.initialComment.body,
}),
});
if (!response.ok) {
throw new APIError(
'Failed to create thread in document',
await errorCauses(response),
);
}
const thread = (await response.json()) as ServerThread;
const threadData: ClientThreadData = serverThreadToClientThread(thread);
this.upsertClientThreadData(threadData);
this.notifySubscribers();
this.ping(threadData.id);
return threadData;
};
public getThread(threadId: string) {
const thread = this.threads.get(threadId);
if (!thread) {
throw new Error('Thread not found');
}
return thread;
}
public getThreads(): Map<string, ClientThreadData> {
if (!this.docAuth.canSee) {
return new Map();
}
return this.threads;
}
public async refreshThread(threadId: string) {
const response = await fetchAPI(
`documents/${this.docId}/threads/${threadId}/`,
{ method: 'GET' },
);
// If not OK and 404, the thread might have been deleted but the
// thread modal is still open, so we close it to avoid side effects
if (response.status === 404) {
// use escape key event to close the thread modal
document.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
bubbles: true,
cancelable: true,
}),
);
await this.refreshThreads();
return;
}
if (!response.ok) {
throw new APIError(
`Failed to fetch thread ${threadId}`,
await errorCauses(response),
);
}
const serverThread = (await response.json()) as ServerThread;
const clientThread = serverThreadToClientThread(serverThread);
this.upsertClientThreadData(clientThread);
this.notifySubscribers();
}
public async refreshThreads(): Promise<void> {
const response = await fetchAPI(`documents/${this.docId}/threads/`, {
method: 'GET',
});
if (!response.ok) {
throw new APIError(
'Failed to get threads in document',
await errorCauses(response),
);
}
const threads = (await response.json()) as ServerThreadListResponse;
const next = new Map<string, ClientThreadData>();
threads.results.forEach((thread) => {
const threadData: ClientThreadData = serverThreadToClientThread(thread);
next.set(thread.id, threadData);
});
this.threads = next;
this.notifySubscribers();
}
public addComment = async (options: {
comment: {
body: CommentBody;
metadata?: unknown;
};
threadId: string;
}) => {
const { threadId } = options;
const response = await fetchAPI(
`documents/${this.docId}/threads/${threadId}/comments/`,
{
method: 'POST',
body: JSON.stringify({
body: options.comment.body,
}),
},
);
if (!response.ok) {
throw new APIError('Failed to add comment ', await errorCauses(response));
}
const comment = (await response.json()) as ServerComment;
// Optimistically update local thread with new comment
const existing = this.threads.get(threadId);
if (existing) {
const updated: ClientThreadData = {
...existing,
updatedAt: new Date(comment.updated_at || comment.created_at),
comments: [...existing.comments, serverCommentToClientComment(comment)],
};
this.upsertClientThreadData(updated);
this.notifySubscribers();
} else {
// Fallback to fetching the thread if we don't have it locally
await this.refreshThread(threadId);
}
this.ping(threadId);
return serverCommentToClientComment(comment);
};
public updateComment = async (options: {
comment: {
body: CommentBody;
metadata?: unknown;
};
threadId: string;
commentId: string;
}) => {
const { threadId, commentId, comment } = options;
const response = await fetchAPI(
`documents/${this.docId}/threads/${threadId}/comments/${commentId}/`,
{
method: 'PUT',
body: JSON.stringify({
body: comment.body,
}),
},
);
if (!response.ok) {
throw new APIError(
'Failed to add thread to document',
await errorCauses(response),
);
}
await this.refreshThread(threadId);
this.ping(threadId);
return;
};
public deleteComment = async (options: {
threadId: string;
commentId: string;
softDelete?: boolean;
}) => {
const { threadId, commentId } = options;
const response = await fetchAPI(
`documents/${this.docId}/threads/${threadId}/comments/${commentId}/`,
{
method: 'DELETE',
},
);
if (!response.ok) {
throw new APIError(
'Failed to delete comment',
await errorCauses(response),
);
}
// Optimistically remove the comment locally if we have the thread
const existing = this.threads.get(threadId);
if (existing) {
const updated: ClientThreadData = {
...existing,
updatedAt: new Date(),
comments: existing.comments.filter((c) => c.id !== commentId),
};
this.upsertClientThreadData(updated);
this.notifySubscribers();
} else {
// Fallback to fetching the thread
await this.refreshThread(threadId);
}
this.ping(threadId);
};
/**
* UI not implemented
* @param _options
*/
public deleteThread = async (_options: { threadId: string }) => {
const response = await fetchAPI(
`documents/${this.docId}/threads/${_options.threadId}/`,
{
method: 'DELETE',
},
);
if (!response.ok) {
throw new APIError(
'Failed to delete thread',
await errorCauses(response),
);
}
// Remove locally and notify; no need to refetch everything
this.removeThread(_options.threadId);
this.notifySubscribers();
this.ping(_options.threadId);
};
public resolveThread = async (_options: { threadId: string }) => {
const { threadId } = _options;
const response = await fetchAPI(
`documents/${this.docId}/threads/${threadId}/resolve/`,
{ method: 'POST' },
);
if (!response.ok) {
throw new APIError(
'Failed to resolve thread',
await errorCauses(response),
);
}
await this.refreshThreads();
this.ping(threadId);
};
/**
* Todo: Not implemented backend side
* @returns
* @throws
*/
public unresolveThread = async (_options: { threadId: string }) => {
const response = await fetchAPI(
`documents/${this.docId}/threads/${_options.threadId}/unresolve/`,
{ method: 'POST' },
);
if (!response.ok) {
throw new APIError(
'Failed to unresolve thread',
await errorCauses(response),
);
}
await this.refreshThread(_options.threadId);
this.ping(_options.threadId);
};
public addReaction = async (options: {
threadId: string;
commentId: string;
emoji: string;
}) => {
const response = await fetchAPI(
`documents/${this.docId}/threads/${options.threadId}/comments/${options.commentId}/reactions/`,
{
method: 'POST',
body: JSON.stringify({ emoji: options.emoji }),
},
);
if (!response.ok) {
throw new APIError(
'Failed to add reaction to comment',
await errorCauses(response),
);
}
await this.refreshThread(options.threadId);
this.notifySubscribers();
this.ping(options.threadId);
};
public deleteReaction = async (options: {
threadId: string;
commentId: string;
emoji: string;
}) => {
const response = await fetchAPI(
`documents/${this.docId}/threads/${options.threadId}/comments/${options.commentId}/reactions/`,
{ method: 'DELETE', body: JSON.stringify({ emoji: options.emoji }) },
);
if (!response.ok) {
throw new APIError(
'Failed to delete reaction from comment',
await errorCauses(response),
);
}
await this.refreshThread(options.threadId);
this.notifySubscribers();
this.ping(options.threadId);
};
}
const serverReactionToReactionData = (r: ServerReaction) => {
return {
emoji: r.emoji,
createdAt: new Date(r.created_at),
userIds: r.users?.map((user) =>
encodeURIComponent(user.full_name || ''),
) || [''],
};
};
const serverCommentToClientComment = (c: ServerComment): ClientCommentData => ({
type: 'comment',
id: c.id,
userId: encodeURIComponent(c.user?.full_name || ''),
body: c.body,
createdAt: new Date(c.created_at),
updatedAt: new Date(c.updated_at),
reactions: (c.reactions ?? []).map(serverReactionToReactionData),
metadata: { abilities: c.abilities },
});
const serverThreadToClientThread = (t: ServerThread): ClientThreadData => ({
type: 'thread',
id: t.id,
createdAt: new Date(t.created_at),
updatedAt: new Date(t.updated_at),
comments: (t.comments ?? []).map(serverCommentToClientComment),
resolved: t.resolved,
resolvedUpdatedAt: t.resolved_updated_at
? new Date(t.resolved_updated_at)
: undefined,
resolvedBy: t.resolved_by || undefined,
metadata: { abilities: t.abilities, metadata: t.metadata },
});

View File

@@ -1,94 +0,0 @@
import { ThreadStoreAuth } from '@blocknote/core/comments';
import { ClientCommentData, ClientThreadData } from './types';
export class DocsThreadStoreAuth extends ThreadStoreAuth {
constructor(
private readonly userId: string,
public canSee: boolean,
) {
super();
}
canCreateThread(): boolean {
return true;
}
canAddComment(_thread: ClientThreadData): boolean {
return true;
}
canUpdateComment(comment: ClientCommentData): boolean {
if (
comment.metadata.abilities.partial_update &&
comment.userId === this.userId
) {
return true;
}
return false;
}
canDeleteComment(comment: ClientCommentData): boolean {
if (comment.metadata.abilities.destroy) {
return true;
}
return false;
}
canDeleteThread(thread: ClientThreadData): boolean {
if (thread.metadata.abilities.destroy) {
return true;
}
return false;
}
canResolveThread(thread: ClientThreadData): boolean {
if (thread.metadata.abilities.resolve) {
return true;
}
return false;
}
/**
* Not implemented backend side
* @param _thread
* @returns
*/
canUnresolveThread(_thread: ClientThreadData): boolean {
return false;
}
canAddReaction(comment: ClientCommentData, emoji?: string): boolean {
if (!comment.metadata.abilities.reactions) {
return false;
}
if (!emoji) {
return true;
}
return !comment.reactions.some(
(reaction) =>
reaction.emoji === emoji && reaction.userIds.includes(this.userId),
);
}
canDeleteReaction(comment: ClientCommentData, emoji?: string): boolean {
if (!comment.metadata.abilities.reactions) {
return false;
}
if (!emoji) {
return true;
}
return comment.reactions.some(
(reaction) =>
reaction.emoji === emoji && reaction.userIds.includes(this.userId),
);
}
}

View File

@@ -1,3 +0,0 @@
export * from './CommentToolbarButton';
export * from './styles';
export * from './useComments';

View File

@@ -1,214 +0,0 @@
import { css } from 'styled-components';
export const cssComments = (
canSeeComment: boolean,
currentUserAvatarUrl?: string,
) => css`
& .--docs--main-editor,
& .--docs--main-editor .ProseMirror {
// Comments marks in the editor
.bn-editor {
.bn-thread-mark:not([data-orphan='true']),
.bn-thread-mark-selected:not([data-orphan='true']) {
background: ${canSeeComment ? '#EDB40066' : 'transparent'};
color: var(--c--theme--colors--greyscale-700);
}
}
em-emoji-picker {
box-shadow: 0px 6px 18px 0px #00001229;
min-height: 420px;
}
// Thread modal
.bn-thread {
width: 400px;
padding: 8px;
box-shadow: 0px 6px 18px 0px #00001229;
margin-left: 20px;
gap: 0;
overflow: auto;
max-height: 500px;
.bn-default-styles {
font-family: var(--c--theme--font--families--base);
}
.bn-block {
font-size: 14px;
}
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before {
font-style: normal;
font-size: 14px;
}
// Remove tooltip
*[role='tooltip'] {
display: none;
}
.bn-thread-comment {
padding: 8px;
& .bn-editor {
padding-left: 32px;
.bn-inline-content {
color: var(--c--theme--colors--greyscale-700);
}
}
// Emoji
& .bn-badge-group {
padding-left: 32px;
.bn-badge label {
padding: 0 4px;
background: none;
border: 1px solid var(--c--theme--colors--greyscale-300);
border-radius: 4px;
height: 24px;
}
}
// Top bar (Name / Date / Actions) when actions displayed
&:has(.bn-comment-actions) {
& > .mantine-Group-root {
max-width: 70%;
right: 0.3rem !important;
top: 0.3rem !important;
}
.bn-menu-dropdown {
box-shadow: 0px 0px 6px 0px #0000911a;
}
}
// Top bar (Name / Date / Actions)
& > .mantine-Group-root {
flex-wrap: nowrap;
max-width: 100%;
gap: 0.5rem;
// Date
span.mantine-focus-auto {
display: inline-block;
}
.bn-comment-actions {
background: transparent;
border: none;
.mantine-Button-root {
background-color: transparent;
&:hover {
background-color: var(--c--theme--colors--greyscale-100);
}
}
button[role='menuitem'] svg {
color: var(--c--theme--colors--greyscale-600);
}
}
& svg {
color: var(--c--theme--colors--info-600);
}
}
// Actions button edit comment
.bn-container + .bn-comment-actions-wrapper {
.bn-comment-actions {
flex-direction: row-reverse;
background: none;
border: none;
gap: 0.4rem !important;
& > button {
height: 24px;
padding-inline: 4px;
&[data-test='save'] {
border: 1px solid var(--c--theme--colors--info-600);
background: var(--c--theme--colors--info-600);
color: white;
}
&[data-test='cancel'] {
background: white;
border: 1px solid var(--c--theme--colors--greyscale-300);
color: var(--c--theme--colors--info-600);
}
}
}
}
}
// Input to add a new comment
.bn-thread-composer,
&:has(> .bn-comment-editor + .bn-comment-actions-wrapper) {
padding: 0.5rem 8px;
flex-direction: row;
gap: 10px;
.bn-container.bn-comment-editor {
min-width: 0;
}
&::before {
content: '';
width: 26px;
height: 26px;
flex: 0 0 26px;
background-image: ${currentUserAvatarUrl
? `url("${currentUserAvatarUrl}")`
: 'none'};
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
}
// Actions button send comment
.bn-thread-composer .bn-comment-actions-wrapper,
&:not(.selected) .bn-comment-actions-wrapper {
flex-basis: fit-content;
.bn-action-toolbar.bn-comment-actions {
border: none;
button {
font-size: 0;
background: var(--c--theme--colors--info-600);
width: 24px;
height: 24px;
padding: 0;
&:disabled {
background: var(--c--theme--colors--greyscale-300);
}
& .mantine-Button-label::before {
content: '🡡';
font-size: 13px;
color: var(--c--theme--colors--greyscale-100);
}
}
}
}
// Input first comment
&:not(.selected) {
gap: 0.5rem;
.bn-container.bn-comment-editor {
min-width: 0;
.ProseMirror.bn-editor {
cursor: text;
}
}
}
}
}
`;

View File

@@ -1,55 +0,0 @@
import { CommentData, ThreadData } from '@blocknote/core/comments';
import { UserLight } from '@/features/auth';
export interface CommentAbilities {
destroy: boolean;
update: boolean;
partial_update: boolean;
retrieve: boolean;
reactions: boolean;
}
export interface ThreadAbilities {
destroy: boolean;
update: boolean;
partial_update: boolean;
retrieve: boolean;
resolve: boolean;
}
export interface ServerReaction {
emoji: string;
created_at: string;
users: UserLight[] | null;
}
export interface ServerComment {
id: string;
user: UserLight | null;
body: unknown;
created_at: string;
updated_at: string;
reactions: ServerReaction[];
abilities: CommentAbilities;
}
export interface ServerThread {
id: string;
created_at: string;
updated_at: string;
user: UserLight | null;
resolved: boolean;
resolved_updated_at: string | null;
resolved_by: string | null;
metadata: unknown;
comments: ServerComment[];
abilities: ThreadAbilities;
}
export type ClientCommentData = Omit<CommentData, 'metadata'> & {
metadata: { abilities: CommentAbilities };
};
export type ClientThreadData = Omit<ThreadData, 'metadata'> & {
metadata: { abilities: ThreadAbilities; metadata: unknown };
};

View File

@@ -1,33 +0,0 @@
import { useEffect, useMemo } from 'react';
import { User } from '@/features/auth';
import { Doc, useProviderStore } from '@/features/docs/doc-management';
import { DocsThreadStore } from './DocsThreadStore';
import { DocsThreadStoreAuth } from './DocsThreadStoreAuth';
export function useComments(
docId: Doc['id'],
canComment: boolean,
user: User | null | undefined,
) {
const { provider } = useProviderStore();
const threadStore = useMemo(() => {
return new DocsThreadStore(
docId,
provider?.awareness ?? undefined,
new DocsThreadStoreAuth(
encodeURIComponent(user?.full_name || ''),
canComment,
),
);
}, [docId, canComment, provider?.awareness, user?.full_name]);
useEffect(() => {
return () => {
threadStore?.destroy();
};
}, [threadStore]);
return threadStore;
}

View File

@@ -136,10 +136,6 @@ export const cssEditor = css`
border-left: none;
}
.bn-toolbar {
max-width: 95vw;
}
/**
* Quotes
*/

View File

@@ -80,7 +80,6 @@ export interface Doc {
children_create: boolean;
children_list: boolean;
collaboration_auth: boolean;
comment: boolean;
destroy: boolean;
duplicate: boolean;
favorite: boolean;

View File

@@ -108,12 +108,6 @@ export const DocShareAddMemberList = ({
afterInvite?.();
setIsLoading(false);
};
const inviteLabel =
selectedUsers.length === 1
? t('Invite {{name}}', {
name: selectedUsers[0].full_name || selectedUsers[0].email,
})
: t('Invite {{count}} members', { count: selectedUsers.length });
return (
<Box
@@ -149,11 +143,7 @@ export const DocShareAddMemberList = ({
currentRole={invitationRole}
onSelectRole={setInvitationRole}
/>
<Button
onClick={() => void onInvite()}
disabled={isLoading}
aria-label={inviteLabel}
>
<Button onClick={() => void onInvite()} disabled={isLoading}>
{t('Invite')}
</Button>
</Box>

View File

@@ -1,5 +1,4 @@
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
@@ -11,7 +10,6 @@ type Props = {
onRemoveUser?: (user: User) => void;
};
export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
const { t } = useTranslation();
const { spacingsTokens, colorsTokens, fontSizesTokens } =
useCunninghamTheme();
@@ -44,9 +42,6 @@ export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
size="nano"
onClick={() => onRemoveUser?.(user)}
icon={<Icon $variation="600" $size="sm" iconName="close" />}
aria-label={t('Remove {{name}} from the invite list', {
name: user.full_name || user.email,
})}
/>
</Box>
);

View File

@@ -4,7 +4,9 @@ import {
QuickSearchItemContentProps,
} from '@/components/quick-search';
import { useCunninghamTheme } from '@/cunningham';
import { User, UserAvatar } from '@/features/auth';
import { User } from '@/features/auth';
import { UserAvatar } from './UserAvatar';
type Props = {
user: User;
@@ -34,7 +36,7 @@ export const SearchUserRow = ({
className="--docs--search-user-row"
>
<UserAvatar
fullName={user.full_name || user.email}
user={user}
background={
isInvitation ? colorsTokens['greyscale-400'] : undefined
}

View File

@@ -0,0 +1,62 @@
import { css } from 'styled-components';
import { Text } from '@/components';
import { tokens } from '@/cunningham';
import { User } from '@/features/auth';
const colors = tokens.themes.default.theme.colors;
const avatarsColors = [
colors['blue-500'],
colors['brown-500'],
colors['cyan-500'],
colors['gold-500'],
colors['green-500'],
colors['olive-500'],
colors['orange-500'],
colors['pink-500'],
colors['purple-500'],
colors['yellow-500'],
];
const getColorFromName = (name: string) => {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return avatarsColors[Math.abs(hash) % avatarsColors.length];
};
type Props = {
user: User;
background?: string;
};
export const UserAvatar = ({ user, background }: Props) => {
const name = user.full_name || user.email || '?';
const splitName = name?.split(' ');
return (
<Text
className="--docs--user-avatar"
$align="center"
$color="rgba(255, 255, 255, 0.9)"
$justify="center"
$background={background || getColorFromName(name)}
$width="24px"
$height="24px"
$radius="50%"
$size="10px"
$textAlign="center"
$textTransform="uppercase"
$weight={600}
$css={css`
border: 1px solid rgba(255, 255, 255, 0.5);
contain: content;
`}
>
{splitName[0]?.charAt(0)}
{splitName?.[1]?.charAt(0)}
</Text>
);
};

View File

@@ -77,9 +77,7 @@ export const DocVersionEditor = ({
return (
<DocEditorContainer
docHeader={<DocVersionHeader />}
docEditor={
<BlockNoteReader initialContent={initialContent} docId={version.id} />
}
docEditor={<BlockNoteReader initialContent={initialContent} />}
isDeletedDoc={false}
readOnly={true}
/>

View File

@@ -5,12 +5,23 @@ import {
PanelGroup,
PanelResizeHandle,
} from 'react-resizable-panels';
import { createGlobalStyle } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
interface PanelStyleProps {
$isResizing: boolean;
}
const PanelStyle = createGlobalStyle<PanelStyleProps>`
${({ $isResizing }) => $isResizing && `body * { transition: none !important; }`}
`;
// Convert a target pixel width to a percentage of the current viewport width.
const pxToPercent = (px: number) => {
return (px / window.innerWidth) * 100;
// react-resizable-panels expects sizes in %, not px.
const calculateDefaultSize = (targetWidth: number) => {
const windowWidth = window.innerWidth;
return (targetWidth / windowWidth) * 100;
};
type ResizableLeftPanelProps = {
@@ -26,49 +37,60 @@ export const ResizableLeftPanel = ({
minPanelSizePx = 300,
maxPanelSizePx = 450,
}: ResizableLeftPanelProps) => {
const [isResizing, setIsResizing] = useState(false);
const { colorsTokens } = useCunninghamTheme();
const ref = useRef<ImperativePanelHandle>(null);
const savedWidthPxRef = useRef<number>(minPanelSizePx);
const resizeTimeoutRef = useRef<number | undefined>(undefined);
const [panelSizePercent, setPanelSizePercent] = useState(() =>
pxToPercent(minPanelSizePx),
);
const [minPanelSize, setMinPanelSize] = useState(0);
const [maxPanelSize, setMaxPanelSize] = useState(0);
const minPanelSizePercent = pxToPercent(minPanelSizePx);
const maxPanelSizePercent = Math.min(pxToPercent(maxPanelSizePx), 40);
// Keep pixel width constant on window resize
// Single resize listener that handles both panel size updates and transition disabling
useEffect(() => {
const handleResize = () => {
const newPercent = pxToPercent(savedWidthPxRef.current);
setPanelSizePercent(newPercent);
if (ref.current) {
ref.current.resize?.(newPercent - (ref.current.getSize() || 0));
// Update panel sizes (px -> %)
const min = Math.round(calculateDefaultSize(minPanelSizePx));
const max = Math.round(
Math.min(calculateDefaultSize(maxPanelSizePx), 40),
);
setMinPanelSize(min);
setMaxPanelSize(max);
// Temporarily disable transitions to avoid flicker
setIsResizing(true);
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = window.setTimeout(() => {
setIsResizing(false);
}, 150);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, []);
const handleResize = (sizePercent: number) => {
const widthPx = (sizePercent / 100) * window.innerWidth;
savedWidthPxRef.current = widthPx;
setPanelSizePercent(sizePercent);
};
}, [minPanelSizePx, maxPanelSizePx]);
return (
<>
<PanelGroup direction="horizontal">
<PanelStyle $isResizing={isResizing} />
<PanelGroup
autoSaveId="docs-left-panel-persistence"
direction="horizontal"
>
<Panel
ref={ref}
order={0}
defaultSize={panelSizePercent}
minSize={minPanelSizePercent}
maxSize={maxPanelSizePercent}
onResize={handleResize}
defaultSize={minPanelSize}
minSize={minPanelSize}
maxSize={maxPanelSize}
>
{leftPanel}
</Panel>

View File

@@ -188,7 +188,6 @@ export class ApiPlugin implements WorkboxPlugin {
children_create: true,
children_list: true,
collaboration_auth: true,
comment: true,
destroy: true,
duplicate: true,
favorite: true,

View File

@@ -77,6 +77,7 @@
"Docx": "Docx",
"Download": "Pellgargañ",
"Download anyway": "Pellgargañ memestra",
"Download your document in a .docx, .odt or .pdf format.": "Pellgargañ ho restr dindan ur stumm .docx, .odt pe .pdf.",
"Duplicate": "Eilañ",
"Editing": "Oc'h aozañ",
"Editor": "Embanner",
@@ -243,25 +244,16 @@
"translation": {
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" ist bereits zum Dokument eingeladen.",
"\"{{email}}\" is already member of the document.": "\"{{email}}\" ist bereits Mitglied des Dokuments.",
"401 Unauthorized": "401 Nicht autorisiert",
"A new way to organize knowledge.": "Wissen organisieren: mal ganz anders.",
"AI Actions": "KI-Aktionen",
"AI seems busy! Please try again.": "KI scheint beschäftigt! Bitte versuchen Sie es erneut.",
"Access Denied - Error 403": "Zugriff verweigert - Fehler 403",
"Access Requests": "Zugriffsanfragen",
"Access request sent successfully.": "Zugriffsanfrage erfolgreich versendet.",
"Accessible to anyone": "Für alle zugänglich",
"Accessible to authenticated users": "Für authentifizierte Benutzer zugänglich",
"Actions for {{title}}": "Aktionen für {{title}}",
"Add": "Hinzufügen",
"Add PDF": "PDF hinzufügen",
"Add a callout block": "Hinweis-Block hinzufügen",
"Add a sub page": "Eine Unterseite hinzufügen",
"Add emoji": "Dokumenten-Symbol hinzufügen",
"Administrator": "Administrator",
"Alert deleted document": "Warnmeldung bei gelöschtem Dokument",
"All docs": "Alle Dokumente",
"An error occurred while restoring the document: {{error}}": "Fehler beim Wiederherstellen des Dokuments: {{error}}",
"An uncompromising writing experience.": "Ein kompromissloses Schreiberlebnis.",
"Analyzing file...": "Analysiere Datei...",
"Anonymous": "Gast",
@@ -270,46 +262,26 @@
"Anyone with the link can see the document": "Jeder mit dem Link kann das Dokument ansehen",
"Anyone with the link can view the document if they are logged in": "Jeder mit dem Link kann das Dokument ansehen, wenn er angemeldet ist",
"Approve": "Freigeben",
"As this is a sub-document, please request access to the parent document to enable these features.": "Dies ist ein untergeordnetes Dokument, fordern Sie bitte den Zugriff auf das übergeordnete Dokument an, um diese Funktionen zu ermöglichen.",
"Available soon": "Bald verfügbar",
"Back to homepage": "Zurück zur Startseite",
"Banner image": "Bannerbild",
"Beautify": "Verschönern",
"By moving this document to <strong>{{targetDocumentTitle}}</strong>, it will lose its current access rights and inherit the permissions of that document. <strong>This access change cannot be undone.</strong>": "Wenn Sie dieses Dokument unterordnen, übernimmt es die Zugriffsrechte von <strong>{{targetDocumentTitle}}</strong>. <strong>Diese Änderung kann nicht rückgängig gemacht werden.</strong>",
"Callout": "Hinweis",
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
"Cancel": "Abbrechen",
"Cancel the deletion": "Löschvorgang abbrechen",
"Cancel the download": "Download abbrechen",
"Close the access request modal": "Zugriffsanforderungsfenster schließen",
"Close the delete modal": "Lösch-Fenster schließen",
"Close the download modal": "Export-Fenster schließen",
"Close the search modal": "Such-Fenster schließen",
"Close the share modal": "Teilen-Fenster schließen",
"Close the version history modal": "Versionsverlauf-Fenster schließen",
"Collaborate": "Zusammenarbeiten",
"Collaborate and write in real time, without layout constraints.": "In Echtzeit und ohne Layout-Beschränkungen schreiben und zusammenarbeiten.",
"Collaborative writing, Simplified.": "Kollaboratives Schreiben, vereinfacht.",
"Confirm": "Bestätigen",
"Connected": "Angemeldet",
"Contains {{count}} sub-documents_many": "Enthält {{count}} Unter-Dokumente",
"Contains {{count}} sub-documents_one": "Enthält {{count}} Unter-Dokument",
"Contains {{count}} sub-documents_other": "Enthält {{count}} Unter-Dokumente",
"Content modal to explain why the user cannot edit": "Fenster um zu erklären, warum der Benutzer nicht bearbeiten kann",
"Content modal to export the document": "Inhalte zum Exportieren des Dokuments",
"Convert Markdown": "Markdown konvertieren",
"Copied to clipboard": "In die Zwischenablage kopiert",
"Copy as {{format}}": "Als {{format}} kopieren",
"Copy link": "Link kopieren",
"Correct": "Korrigieren",
"Create a new sub-doc": "Erzeuge ein neues Unter-Dokument",
"Current doc": "Aktuelles Dokument",
"Days remaining": "Tage verbleibend",
"Days remaining:": "Tage verbleibend:",
"Delete": "Löschen",
"Delete a doc": "Dokument löschen",
"Delete document": "Dokument löschen",
"Delete sub-document": "Unter-Dokument löschen",
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
"Docs": "Docs",
"Docs Logo": "Docs-Logo",
@@ -318,39 +290,24 @@
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs bietet ein intuitives Schreiberlebnis. Seine minimalistische Oberfläche bevorzugt Inhalte über Layout, bietet aber das Wesentliche: Medien-Import, Offline-Modus und Tastaturkürzel für mehr Effizienz.",
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Dank Unterseiten, leistungsstarker Suche und der Möglichkeit, wichtige Dokumente zu fixieren, verwandelt Docs Ihre Dokumente in Wissensdatenbanken.",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
"Document access mode": "Zugriffsrechte",
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
"Document deleted": "Dokument wurde gelöscht",
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
"Document editor": "Dokumenten-Editor",
"Document owner": "Besitzer des Dokuments",
"Document role text": "Rolle",
"Document sections": "Linke Seitenleiste",
"Document title": "Titel des Dokuments",
"Document tree": "Dokumentenbaum",
"Document version viewer": "Dokumentenversion-Betrachter",
"Document visibility": "Sichtbarkeit",
"Documents grid": "Dokumentenübersicht",
"Docx": "Docx",
"Download": "Herunterladen",
"Download anyway": "Trotzdem herunterladen",
"Download your document in a .docx, .odt or .pdf format.": "Dokument als DOCX-, ODT- oder PDF-Datei exportieren.",
"Drag and drop status": "Drag and Drop Status",
"Download your document in a .docx, .odt or .pdf format.": "Ihr Dokument als .docx-, .odt- oder .pdf-Datei herunterladen.",
"Duplicate": "Duplizieren",
"Edit document emoji": "Dokumenten-Symbol bearbeiten",
"Editing": "Bearbeiten",
"Editor": "Mitbearbeiter",
"Editor unavailable": "Editor nicht verfügbar",
"Embed a PDF file": "Eine PDF-Datei einbetten",
"Emojify": "Emojifizieren",
"Empty template": "Leere Vorlage",
"Error during delete invitation": "Fehler beim Löschen der Einladung",
"Error during update invitation": "Fehler beim Aktualisieren der Einladung",
"Error while deleting invitation": "Fehler beim Löschen der Einladung",
"Error while removing the request.": "Fehler beim Zurückziehen der Anfrage.",
"Error while updating the member role.": "Fehler beim Aktualisieren der Mitglieds-Rolle.",
"Export": "Exportieren",
"Export the document": "Dokument exportieren",
"Failed to add the member in the document.": "Fehler beim Hinzufügen des Mitglieds zum Dokument.",
"Failed to copy link": "Link konnte nicht kopiert werden",
"Failed to copy to clipboard": "Fehler beim Kopieren in die Zwischenablage",
@@ -363,10 +320,8 @@
"Home": "Start",
"I understand": "Verstanden",
"If a member is editing, his works can be lost.": "Wenn ein Mitglied editiert, können seine Änderungen verloren gehen.",
"If you wish to be able to co-edit in real-time, contact your Information Systems Security Manager about allowing WebSockets.": "Wenn Sie an der kollaborativen Mitarbeit teilhaben möchten, wenden Sie sich an Ihren IT-Support um Web Sockets zuzulassen.",
"Illustration": "Abbildung",
"Image 403": "Bild 403",
"Image: {{title}}": "Abbildung: {{title}}",
"Insufficient access rights to view the document.": "Unzureichende Zugriffsrechte zur Ansicht des Dokuments.",
"Invite": "Einladen",
"It is the card information about the document.": "Es handelt sich um die Karteninformationen zum Dokument.",
@@ -376,50 +331,32 @@
"Last update: {{update}}": "Zuletzt aktualisiert: {{update}}",
"Learn more": "Mehr erfahren",
"Link Copied !": "Link kopiert!",
"Link a doc": "Dokument verlinken",
"Link settings": "Link-Einstellungen",
"Link this doc to another doc": "Verlinke dieses Dokument mit einem anderen Dokument",
"Links": "Links",
"List invitation card": "Einladungsliste anzeigen",
"List members card": "Mitgliederliste anzeigen",
"List request access card": "Karte mit einer Liste der Zugriffsanfragen",
"List search user result card": "Ergebnis der Benutzersuche",
"Load more": "Mehr laden",
"Log in to access the document.": "Zum Zugriff Anmeldung erforderlich.",
"Login": "Anmelden",
"Logo": "Logo",
"Logout": "Abmelden",
"Main content": "Hauptinhalt",
"Modal confirmation to download the attachment": "Modale Bestätigung zum Herunterladen des Anhangs",
"More docs": "Weitere Dokumente",
"More options": "Mehr Einstellungen",
"Move": "Verschieben",
"Move document": "Dokument verschieben",
"Move to my docs": "In \"Meine Dokumente\" verschieben",
"My docs": "Meine Dokumente",
"Name": "Name",
"New doc": "Neues Dokument",
"New sub-doc": "Neues Unter-Dokument",
"New doc": "Neues Dok",
"No active search": "Keine aktive Suche",
"No document found": "Kein Dokument gefunden",
"No documents found": "Keine Dokumente gefunden",
"No text selected": "Kein Text ausgewählt",
"No versions": "Keine Versionen",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Offline?!",
"Only invited people can access": "Nur eingeladene Personen haben Zugriff",
"Open Source": "Open Source",
"Open document {{title}}": "Öffne Dokument: {{title}}",
"Open document: {{title}}": "Öffne Dokument: {{title}}",
"Open invitation actions menu": "Öffne Freigabemenü",
"Open root document": "Öffne Wurzel-Dokument",
"Open the document options": "Öffnen Sie die Dokumentoptionen",
"Open the header menu": "Öffne das Kopfzeilen-Menü",
"Open the menu of actions for the document: {{title}}": "Öffne das Aktionsmenü für das Dokument: {{title}}",
"Open the sharing settings for the document": "Öffne die Freigabeeinstellungen für das Dokument",
"Organize": "Organisieren",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "Andere bearbeiten dieses Dokument. Leider blockieren Sie Web Sockets, die Technologie, die kollaborative Mitarbeit ermöglicht.",
"Others are editing. Your network prevent changes.": "Ihre Änderung konnte nicht übernommen werden, da andere Benutzer diesen Bereich zurzeit bearbeiten.",
"Owner": "Besitzer",
"PDF": "PDF",
@@ -437,26 +374,17 @@
"Reader": "Leser",
"Reading": "Lesen",
"Remove access": "Zugriff entziehen",
"Remove emoji": "Dokumenten-Symbol entfernen",
"Rename": "Umbenennen",
"Rephrase": "Umformulieren",
"Request access": "Zugriff anfragen",
"Reset": "Zurücksetzen",
"Restore": "Wiederherstellen",
"Root document {{title}}": "Hauptdokument {{title}}",
"Search": "Suchen",
"Search by title": "Nach Titel suchen",
"Search docs": "Dokumente durchsuchen",
"Search modal": "Suche Modal",
"Search results": "Suchergebnisse",
"Search user result": "Suchergebnis",
"Select a doc": "Dokument auswählen",
"Select a document": "Dokument auswählen",
"Select a version on the right to restore": "Wählen Sie rechts eine Version zum Wiederherstellen aus",
"Select language": "Sprache wählen",
"Share": "Teilen",
"Share button": "Teilen",
"Share modal content": "Teilen-Fenster Inhalt",
"Share the document": "Dokument teilen",
"Share with {{count}} users_many": "Teilen mit {{count}} Benutzern",
"Share with {{count}} users_one": "Mit {{count}} Benutzern teilen",
@@ -467,7 +395,6 @@
"Shared with {{count}} users_other": "Geteilt mit {{count}} Benutzern",
"Show more": "Mehr zeigen",
"Simple and secure collaboration.": "Einfache und sichere Zusammenarbeit.",
"Simple document icon": "Standard Dokumenten-Symbol",
"Something bad happens, please retry.": "Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
"Start Writing": "Beginne das Schreiben",
"Summarize": "Zusammenfassen",
@@ -475,26 +402,18 @@
"Template": "Vorlage",
"The antivirus has detected an anomaly in your file.": "Das Antivirus hat eine Anomalie in Ihrer Datei festgestellt.",
"The document has been deleted.": "Das Dokument wurde gelöscht.",
"The document has been restored.": "Das Dokument wurde wiederhergestellt.",
"The document visibility has been updated.": "Die Sichtbarkeit des Dokuments wurde aktualisiert.",
"The export failed": "Export fehlgeschlagen",
"The link sharing rules differ from the parent document": "Die Linkfreigabe unterscheidet sich von der des übergeordneten Dokuments",
"This document and <strong>any sub-documents</strong> will be placed in the trashbin. You can restore it within {{days}} days.": "Dieses und <strong>alle untergeordneten Dokumente</strong> werden in den Papierkorb gelegt. Sie können es innerhalb der nächsten {{days}} Tage wiederherstellen.",
"This document will be placed in the trashbin. You can restore it within {{days}} days.": "Dieses Dokument wird in den Papierkorb gelegt. Sie können es innerhalb der nächsten {{days}} Tage wiederherstellen.",
"This file is flagged as unsafe.": "Diese Datei wurde als unsicher markiert.",
"This means you can't edit until others leave.": "Dies bedeutet, dass Sie nicht bearbeiten können, bis andere die Bearbeitung verlassen.",
"This user has access inherited from a parent page.": "Dieser Benutzer erbt den Zugriff von einem übergeordneten Dokument.",
"To facilitate the circulation of documents, Docs allows you to export your content to the most common formats: PDF, Word or OpenDocument.": "Docs erleichtert die Verbreitung von Dokumenten, da es den Export in die gängigsten Formate unterstützt: PDF, Word und OpenDocument.",
"Too many requests. Please wait 60 seconds.": "Zu viele Anfragen. Bitte warten Sie 60 Sekunden.",
"Trashbin": "Papierkorb",
"Type a name or email": "Geben Sie einen Namen oder eine E-Mail-Adresse ein",
"Type the name of a document": "Geben Sie den Namen eines Dokuments ein",
"Unnamed document": "Unbenanntes Dokument",
"Unpin": "Lösen",
"Untitled document": "Unbenanntes Dokument",
"Updated": "Aktualisiert",
"Updated at": "Aktualisiert am",
"Upload PDF": "PDF hochladen",
"Use as prompt": "Als Prompt verwenden",
"Version history": "Versionsverlauf",
"Version restored successfully": "Version erfolgreich wiederhergestellt",
@@ -502,29 +421,17 @@
"Why you can't edit the document?": "Warum können Sie dieses Dokument nicht bearbeiten?",
"Write": "Schreiben",
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Sie sind der einzige Besitzer dieser Gruppe. Machen Sie ein anderes Mitglied zum Gruppenbesitzer, bevor Sie Ihre eigene Rolle ändern oder aus Ihrem Dokument entfernen können.",
"You can view this document but need additional access to see its members or modify settings.": "Sie können dieses Dokument lesen, benötigen aber zusätzliche Rechte, um Mitglieder zu sehen oder Einstellungen zu ändern.",
"You cannot restrict access to a subpage relative to its parent page.": "Unterseiten können keine strengeren Zugriffsbeschränkungen haben als übergeordnete Seiten.",
"You must be at least the administrator of the target document": "Sie müssen dafür mindestens die Rolle \"Administrator\" haben",
"You must be the owner to move the document": "Sie müssen Besitzer des Dokuments sein, um es zu verschieben",
"You're currently viewing a sub-document. To gain access, please request permission from the main document.": "Sie sehen derzeit ein untergeordnetes Dokument. Um Zugriff zu erhalten, fordern Sie bitte Berechtigung am Wurzeldokument an.",
"Your access request for this document is pending.": "Ihre Zugriffsanfrage für dieses Dokument steht noch aus.",
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
"Your {{format}} was downloaded succesfully": "Ihr {{format}} wurde erfolgreich heruntergeladen",
"days_many": "Tage",
"days_one": "Tag",
"days_other": "Tage",
"document": "Dokument",
"embed": "Einbetten",
"file": "Datei",
"home-content-open-source-part1": "Doms ist auf <2>Django Rest Framework</2> und <6>Next.js</6> aufgebaut. Wir verwenden auch <9>Yjs</9> und <13>BlockNote.js</13>, zwei Projekte, die wir mit Stolz sponsern.",
"home-content-open-source-part2": "Sie können Docs ganz einfach selbst hosten (lesen Sie unsere <2>Installationsdokumentation</2>).<br/>Docs verwendet eine <7>Lizenz</7> (MIT), die auf Innovation und Unternehmen zugeschnitten ist.<br/>Beiträge sind willkommen (lesen Sie unsere Roadmap <13>hier</13>).\n",
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird.",
"pdf": "pdf"
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird."
}
},
"en": {
"translation": {
"Contains {{count}} sub-documents_one": "Contains {{count}} sub-document",
"Share with {{count}} users_one": "Share with {{count}} user",
"Shared with {{count}} users_many": "Shared with {{count}} users",
"Shared with {{count}} users_one": "Shared with {{count}} user",
@@ -587,6 +494,7 @@
"Docx": "Docx",
"Download": "Descargar",
"Download anyway": "Descargar de todos modos",
"Download your document in a .docx, .odt or .pdf format.": "Descargue su documento en formato .docx, .odt o .pdf.",
"Editor": "Editor",
"Editor unavailable": "Editor no disponible",
"Emojify": "Emojizar",
@@ -753,9 +661,6 @@
"Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.",
"Confirm": "Confirmez",
"Connected": "Connecté",
"Contains {{count}} sub-documents_many": "Contient {{count}} sous-documents",
"Contains {{count}} sub-documents_one": "Contient {{count}} sous-document",
"Contains {{count}} sub-documents_other": "Contient {{count}} sous-documents",
"Content modal to explain why the user cannot edit": "Contenu modal pour expliquer pourquoi l'utilisateur ne peut pas modifier",
"Content modal to export the document": "Contenu modal pour exporter le document",
"Convert Markdown": "Convertir le Markdown",
@@ -783,13 +688,11 @@
"Document accessible to any connected person": "Document accessible à toute personne connectée",
"Document deleted": "Document supprimé",
"Document duplicated successfully!": "Document dupliqué avec succès !",
"Document editor": "Éditeur de document",
"Document owner": "Propriétaire du document",
"Document role text": "Texte du rôle du document",
"Document sections": "Sections du document",
"Document title": "Titre du document",
"Document tree": "Arborescence du document",
"Document version viewer": "Visionneuse de version du document",
"Document visibility": "Visibilité du document",
"Documents grid": "Grille des documents",
"Docx": "Docx",
@@ -866,7 +769,6 @@
"No documents found": "Aucun document trouvé",
"No text selected": "Aucun texte sélectionné",
"No versions": "Aucune version",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Hors-ligne ?!",
"Only invited people can access": "Seules les personnes invitées peuvent accéder",
@@ -1028,6 +930,7 @@
"Docx": "Docx",
"Download": "Scarica",
"Download anyway": "Scarica comunque",
"Download your document in a .docx, .odt or .pdf format.": "Scarica il tuo documento in formato .docx, .odt o .pdf",
"Editor": "Editor",
"Editor unavailable": "Editor non disponibile",
"Emojify": "Emojify",
@@ -1177,9 +1080,6 @@
"Collaborative writing, Simplified.": "Gezamenlijk schrijven, Vereenvoudigd.",
"Confirm": "Bevestigen",
"Connected": "Ingelogd",
"Contains {{count}} sub-documents_many": "Bevat {{count}} subdocumenten",
"Contains {{count}} sub-documents_one": "Bevat {{count}} subdocumenten",
"Contains {{count}} sub-documents_other": "Bevat {{count}} subdocumenten",
"Content modal to explain why the user cannot edit": "Content modal om uit te leggen waarom de gebruiker niet kan bewerken",
"Content modal to export the document": "Content venster om document te exporteren",
"Convert Markdown": "Converteer naar Markdown formaat",
@@ -1207,19 +1107,17 @@
"Document accessible to any connected person": "Document is toegankelijk voor ieder ingelogde persoon",
"Document deleted": "Document verwijderd",
"Document duplicated successfully!": "Document met succes gedupliceerd!",
"Document editor": "Documenteditor",
"Document owner": "Document eigenaar",
"Document role text": "Document roluitleg",
"Document sections": "Document secties",
"Document title": "Documenttitel",
"Document tree": "Boomstructuur document",
"Document version viewer": "Document versie viewer",
"Document visibility": "Document toegankelijkheid",
"Documents grid": "Documenten overzicht",
"Docx": "Docx",
"Download": "Download",
"Download anyway": "Download alsnog",
"Download your document in a .docx, .odt or .pdf format.": "Download uw document in een .docx, .odt of .pdf formaat.",
"Download your document in a .docx, .odt or .pdf format.": "Download jouw document in .docx, .odt of .pdf formaat.",
"Drag and drop status": "Drag & drop status",
"Duplicate": "Dupliceer",
"Edit document emoji": "Bewerk document emoji",
@@ -1290,7 +1188,6 @@
"No documents found": "Geen documenten gevonden",
"No text selected": "Geen tekst geselecteerd",
"No versions": "Geen versies",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Offline?!",
"Only invited people can access": "Alleen uitgenodigde gebruikers hebben toegang",
@@ -1426,7 +1323,6 @@
"Add PDF": "Добавить PDF-файл",
"Add a callout block": "Добавить блок выноски",
"Add a sub page": "Добавить вложенную страницу",
"Add emoji": "Добавь эмодзи",
"Administrator": "Администратор",
"Alert deleted document": "Оповещение об удалённом документе",
"All docs": "Все документы",
@@ -1461,9 +1357,6 @@
"Collaborative writing, Simplified.": "Простой совместный доступ к документам.",
"Confirm": "Подтвердить",
"Connected": "Подключённая",
"Contains {{count}} sub-documents_many": "Содержит {{count}} вложенных документов",
"Contains {{count}} sub-documents_one": "Содержит {{count}} вложенных документов",
"Contains {{count}} sub-documents_other": "Содержит {{count}} вложенных документов",
"Content modal to explain why the user cannot edit": "Пояснение, почему пользователь не может редактировать документ",
"Content modal to export the document": "Подтверждение экспорта документа",
"Convert Markdown": "Преобразовать разметку",
@@ -1491,22 +1384,19 @@
"Document accessible to any connected person": "Документ доступен всем, кто присоединится",
"Document deleted": "Документ удалён",
"Document duplicated successfully!": "Документ успешно дублирован!",
"Document editor": "Редактор документа",
"Document owner": "Владелец документа",
"Document role text": "Текст роли документа",
"Document sections": "Разделы документа",
"Document title": "Заголовок документа",
"Document tree": "Иерархия документа",
"Document version viewer": "Просмотрщик версий документа",
"Document visibility": "Видимость документа",
"Documents grid": "Сетка документов",
"Docx": "Docx",
"Download": "Загрузить",
"Download anyway": "Всё равно загрузить",
"Download your document in a .docx, .odt or .pdf format.": "Загрузить документ в формате .docx, .odt или .pdf.",
"Download your document in a .docx, .odt or .pdf format.": "Загрузите свой документ в формате .docx, .odt или .pdf.",
"Drag and drop status": "Состояние перетаскивания",
"Duplicate": "Дублировать",
"Edit document emoji": "Изменить документ emoji",
"Editing": "Редактирование",
"Editor": "Редактор",
"Editor unavailable": "Редактор недоступен",
@@ -1574,7 +1464,6 @@
"No documents found": "Документы не найдены",
"No text selected": "Текст не выбран",
"No versions": "Нет версий",
"ODT": "ODT",
"OK": "ОК",
"Offline ?!": "Не в сети?!",
"Only invited people can access": "Имеют доступ только приглашенные люди",
@@ -1685,9 +1574,9 @@
"document": "документ",
"embed": "вложение",
"file": "файл",
"home-content-open-source-part1": "текст ещё не готов",
"home-content-open-source-part2": "текст ещё не готов",
"home-content-open-source-part3": "текст ещё не готов",
"home-content-open-source-part1": "home-content-open-source-part1",
"home-content-open-source-part2": "home-content-open-source-part2",
"home-content-open-source-part3": "home-content-open-source-part3",
"pdf": "pdf"
}
},
@@ -1753,6 +1642,7 @@
"Docx": "Docx",
"Download": "İndir",
"Download anyway": "Yine de indir",
"Download your document in a .docx, .odt or .pdf format.": "Belgenizi .docx, .odt veya .pdf formatında indirin.",
"Editor": "Editör",
"Editor unavailable": "Editör mevcut değil",
"Emojify": "Emojileştir",
@@ -1813,7 +1703,6 @@
"Add PDF": "Додати файл PDF",
"Add a callout block": "Додати блок винесення",
"Add a sub page": "Додати вкладену сторінку",
"Add emoji": "Додати емодзі",
"Administrator": "Адміністратор",
"Alert deleted document": "Сповіщення про видалений документ",
"All docs": "Всі документи",
@@ -1848,9 +1737,6 @@
"Collaborative writing, Simplified.": "Простий спільний доступ до документів.",
"Confirm": "Підтвердити",
"Connected": "Під'єднане",
"Contains {{count}} sub-documents_many": "Містить {{count}} вкладених документів",
"Contains {{count}} sub-documents_one": "Містить {{count}} вкладених документів",
"Contains {{count}} sub-documents_other": "Містить {{count}} вкладених документів",
"Content modal to explain why the user cannot edit": "Пояснення, чому користувач не може редагувати",
"Content modal to export the document": "Підтвердження експорту документа",
"Convert Markdown": "Перетворити розмітку",
@@ -1878,22 +1764,19 @@
"Document accessible to any connected person": "Документ, доступний для будь-якої особи, що приєдналася",
"Document deleted": "Документ видалено",
"Document duplicated successfully!": "Документ успішно продубльовано!",
"Document editor": "Редактор документа",
"Document owner": "Власник документа",
"Document role text": "Текст ролі документа",
"Document sections": "Розділи документу",
"Document title": "Назва документа",
"Document tree": "Дерево документа",
"Document version viewer": "Переглядач версії документа",
"Document visibility": "Видимість документа",
"Documents grid": "Сітка документів",
"Docx": "Docx",
"Download": "Завантажити",
"Download anyway": "Все одно завантажити",
"Download your document in a .docx, .odt or .pdf format.": "Завантажити документ у форматі .docx, .odt або .pdf.",
"Download your document in a .docx, .odt or .pdf format.": "Завантажте ваш документ у форматі .docx, .odt або .pdf.",
"Drag and drop status": "Стан перетягування",
"Duplicate": "Дублювати",
"Edit document emoji": "Редагувати документ емодзі",
"Editing": "Редагування",
"Editor": "Редактор",
"Editor unavailable": "Редактор недоступний",
@@ -1961,7 +1844,6 @@
"No documents found": "Документів не знайдено",
"No text selected": "Текст не вибрано",
"No versions": "Немає версій",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Не в мережі?!",
"Only invited people can access": "Лише запрошені люди можуть мати доступ",
@@ -2072,9 +1954,9 @@
"document": "документ",
"embed": "вкладення",
"file": "файл",
"home-content-open-source-part1": "текст ще не готовий",
"home-content-open-source-part2": "текст ще не готовий",
"home-content-open-source-part3": "текст ще не готовий",
"home-content-open-source-part1": "home-content-open-source-part1",
"home-content-open-source-part2": "home-content-open-source-part2",
"home-content-open-source-part3": "home-content-open-source-part3",
"pdf": "pdf"
}
},
@@ -2136,6 +2018,7 @@
"Docx": "Doc",
"Download": "下载",
"Download anyway": "仍要下载",
"Download your document in a .docx, .odt or .pdf format.": "以doc, odt或者pdf格式下载。",
"Editing": "正在编辑",
"Editor": "编辑者",
"Editor unavailable": "编辑功能不可用",

View File

@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.10.0",
"version": "3.9.0",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",

View File

@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "3.10.0",
"version": "3.9.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.10.0",
"version": "3.9.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.10.0",
"version": "3.9.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -16,7 +16,7 @@
"node": ">=22"
},
"dependencies": {
"@blocknote/server-util": "0.42.3",
"@blocknote/server-util": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@2183",
"@hocuspocus/server": "3.4.0",
"@sentry/node": "10.22.0",
"@sentry/profiling-node": "10.22.0",
@@ -30,7 +30,7 @@
"yjs": "*"
},
"devDependencies": {
"@blocknote/core": "0.42.3",
"@blocknote/core": "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2183",
"@hocuspocus/provider": "3.4.0",
"@types/cors": "2.8.19",
"@types/express": "5.0.5",

View File

@@ -1070,12 +1070,11 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blocknote/code-block@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/code-block/-/code-block-0.42.3.tgz#7b1a3ed0b4f2d75835c422c04f52e824aa0845cf"
integrity sha512-kPdHABXJdH7lxB1Fxqg/bxWmtO/5y3REgRcuppEpCkrfXlwV+RPCChCU2jnMTUBe6CZyFQwum6/D9oUGVJKRqw==
"@blocknote/code-block@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@2183#ccc7b2181cce68d16411e3f245f13bcdc0d50be9"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@shikijs/core" "^3.13.0"
"@shikijs/engine-javascript" "^3.13.0"
"@shikijs/langs" "^3.13.0"
@@ -1083,14 +1082,13 @@
"@shikijs/themes" "^3.13.0"
"@shikijs/types" "^3.13.0"
"@blocknote/core@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.42.3.tgz#2ac1654a04df65d4618440e520ad187d0c070169"
integrity sha512-wtZki6Gok5Ac9Ek6QTQztcDymstEQgVCisJwiUZTWXh8CD4UKfnIxM7C9+6eEnZMmQ8GNTvRf1HXFl+E4N78VA==
"@blocknote/core@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880#90ca822cfee74cb17612fe66bf23ecca8ebfa68b"
dependencies:
"@emoji-mart/data" "^1.2.1"
"@shikijs/types" "3.13.0"
"@tiptap/core" "^3.11.0"
"@tiptap/core" "^3.10.2"
"@tiptap/extension-bold" "^3.7.2"
"@tiptap/extension-code" "^3.7.2"
"@tiptap/extension-gapcursor" "^3.7.2"
@@ -1102,7 +1100,7 @@
"@tiptap/extension-strike" "^3.7.2"
"@tiptap/extension-text" "^3.7.2"
"@tiptap/extension-underline" "^3.7.2"
"@tiptap/pm" "^3.11.0"
"@tiptap/pm" "^3.10.2"
emoji-mart "^5.6.0"
fast-deep-equal "^3.1.3"
hast-util-from-dom "^5.0.1"
@@ -1111,7 +1109,7 @@
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.1"
prosemirror-transform "^1.10.5"
prosemirror-transform "^1.10.4"
prosemirror-view "^1.41.3"
rehype-format "^5.0.1"
rehype-parse "^9.0.1"
@@ -1128,89 +1126,157 @@
y-protocols "^1.0.6"
yjs "^13.6.27"
"@blocknote/mantine@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.42.3.tgz#3cd0b13e6ecb40b0b086a4877c2b2361a2fad266"
integrity sha512-xzLweZG1KfFoOp/aSHTXE10IrfEHnhDlP0C2Qt2eNO2IHHa7l8XZJpIGhCoVMsn0yylm91OSynNfTO7JkZZi8w==
"@blocknote/core@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2183":
version "0.42.1"
uid "90ca822cfee74cb17612fe66bf23ecca8ebfa68b"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2183#90ca822cfee74cb17612fe66bf23ecca8ebfa68b"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@emoji-mart/data" "^1.2.1"
"@shikijs/types" "3.13.0"
"@tiptap/core" "^3.10.2"
"@tiptap/extension-bold" "^3.7.2"
"@tiptap/extension-code" "^3.7.2"
"@tiptap/extension-gapcursor" "^3.7.2"
"@tiptap/extension-history" "^3.7.2"
"@tiptap/extension-horizontal-rule" "^3.7.2"
"@tiptap/extension-italic" "^3.7.2"
"@tiptap/extension-link" "^3.7.2"
"@tiptap/extension-paragraph" "^3.7.2"
"@tiptap/extension-strike" "^3.7.2"
"@tiptap/extension-text" "^3.7.2"
"@tiptap/extension-underline" "^3.7.2"
"@tiptap/pm" "^3.10.2"
emoji-mart "^5.6.0"
fast-deep-equal "^3.1.3"
hast-util-from-dom "^5.0.1"
prosemirror-dropcursor "^1.8.2"
prosemirror-highlight "^0.13.0"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.1"
prosemirror-transform "^1.10.4"
prosemirror-view "^1.41.3"
rehype-format "^5.0.1"
rehype-parse "^9.0.1"
rehype-remark "^10.0.1"
rehype-stringify "^10.0.1"
remark-gfm "^4.0.1"
remark-parse "^11.0.0"
remark-rehype "^11.1.2"
remark-stringify "^11.0.0"
unified "^11.0.5"
unist-util-visit "^5.0.0"
uuid "^8.3.2"
y-prosemirror "^1.3.7"
y-protocols "^1.0.6"
yjs "^13.6.27"
"@blocknote/mantine@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@2183#74214c65fcd365e68351217066a9249103e70174"
dependencies:
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/react" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880"
react-icons "^5.5.0"
"@blocknote/react@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.42.3.tgz#c2ec737083e7341a18084c59de2021e64ad5f665"
integrity sha512-YnrQ1uyezDbaxYcFstWOJ2r8BMxqwwEc7QAhrEjCMEyBAiOxSCPnrM4/GE2mOgCS0Xa9wIp2LDoPQP2Syv+2EA==
"@blocknote/react@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880#2703eed2efd77d578c2094c3dea6c832cf28d6bd"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@emoji-mart/data" "^1.2.1"
"@floating-ui/react" "^0.27.16"
"@tiptap/core" "^3.11.0"
"@tiptap/pm" "^3.11.0"
"@tiptap/react" "^3.11.0"
"@tiptap/core" "^3.10.2"
"@tiptap/pm" "^3.10.2"
"@tiptap/react" "^3.10.2"
emoji-mart "^5.6.0"
lodash.merge "^4.6.2"
react-icons "^5.5.0"
"@blocknote/server-util@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/server-util/-/server-util-0.42.3.tgz#113fc33cabc4e6a9fa776183182dfee6fef7e6ff"
integrity sha512-M+jtKeC2aHOYBp6GQ0YR19iv0/0f1HElrrnKwlaSPbwR6bw6tg+yb3yQkaJJioLTpd2X2Z/RwcEvxSJGnlZ81w==
"@blocknote/react@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@2183":
version "0.42.1"
uid "2703eed2efd77d578c2094c3dea6c832cf28d6bd"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@2183#2703eed2efd77d578c2094c3dea6c832cf28d6bd"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@tiptap/core" "^3.11.0"
"@tiptap/pm" "^3.11.0"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@emoji-mart/data" "^1.2.1"
"@floating-ui/react" "^0.27.16"
"@tiptap/core" "^3.10.2"
"@tiptap/pm" "^3.10.2"
"@tiptap/react" "^3.10.2"
emoji-mart "^5.6.0"
lodash.merge "^4.6.2"
react-icons "^5.5.0"
"@blocknote/server-util@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@2183#c3de87251492fec0a6ef1887a40e4cf6076f813b"
dependencies:
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/react" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880"
"@tiptap/core" "^3.10.2"
"@tiptap/pm" "^3.10.2"
jsdom "^25.0.1"
y-prosemirror "^1.3.7"
y-protocols "^1.0.6"
yjs "^13.6.27"
"@blocknote/xl-docx-exporter@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.42.3.tgz#e276b1a22c34a2b5d0837606d7b96cc3acaba92e"
integrity sha512-VpotYcG+fQFzC2gtqTlBJDi0GKQQ6RygzeyzBBDGeMKSH3P72TDKVYVqN4Ert7HxXz41aCLGgtaf6x9zlox26g==
"@blocknote/xl-docx-exporter@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@2183#c8dbd76d3e5344c9b134cc900cf87f5ed9292a33"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/xl-multi-column" "0.42.3"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/xl-multi-column" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@009cb604565b1348591048dc374763b4308e3880"
buffer "^6.0.3"
docx "^9.5.1"
image-meta "^0.2.2"
"@blocknote/xl-multi-column@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-multi-column/-/xl-multi-column-0.42.3.tgz#39fad4ef51f26c4af15423f44334de8edf06fe3c"
integrity sha512-7ylZYlOOVNMJ3u4C07yiE6qr04kcEYnxY3UfcFBSyV8H+N0LHGLFFJIz6JPKWvji4fu5lvbxXqv0IcGbCQ0/cA==
"@blocknote/xl-multi-column@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@009cb604565b1348591048dc374763b4308e3880":
version "0.42.1"
uid c48272172892e1cb19c3c3e229ff7c53679d551a
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@009cb604565b1348591048dc374763b4308e3880#c48272172892e1cb19c3c3e229ff7c53679d551a"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@tiptap/core" "^3.11.0"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/react" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880"
"@tiptap/core" "^3.10.2"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.1"
prosemirror-transform "^1.10.5"
prosemirror-transform "^1.10.4"
prosemirror-view "^1.41.3"
react-icons "^5.5.0"
"@blocknote/xl-odt-exporter@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-odt-exporter/-/xl-odt-exporter-0.42.3.tgz#fee0f142ca8ae5ce8bb969f9de0f79e2a92e1c6a"
integrity sha512-wW1Zxd3Y14IG5X/mi0OBoGV/EFxeO5Alsd0HVsBo0imk+GLSKx2YCU02plUG5l8IOQOUeWBHamm4OT+7sgj9Ow==
"@blocknote/xl-multi-column@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@2183#c48272172892e1cb19c3c3e229ff7c53679d551a"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/xl-multi-column" "0.42.3"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/react" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880"
"@tiptap/core" "^3.10.2"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.1"
prosemirror-transform "^1.10.4"
prosemirror-view "^1.41.3"
react-icons "^5.5.0"
"@blocknote/xl-odt-exporter@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@2183#33af5cd93f776967be5e936be6514f4e380dbcf6"
dependencies:
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/xl-multi-column" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@009cb604565b1348591048dc374763b4308e3880"
"@zip.js/zip.js" "^2.8.8"
buffer "^6.0.3"
image-meta "^0.2.2"
"@blocknote/xl-pdf-exporter@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-pdf-exporter/-/xl-pdf-exporter-0.42.3.tgz#667c0c2756e8300f0dc134718e6d2833947b799f"
integrity sha512-ZPGVHovDWhwu++vkVzDEh6KOoHK6q8iLFo9fGLcQ8oKjNCJoBr344Z42AdMMxcoCDddQmC+5yqzUN8J/9xnE1Q==
"@blocknote/xl-pdf-exporter@https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@2183":
version "0.42.1"
resolved "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@2183#cba855bbc19941f842eab8cfe65089d16a6f0fc7"
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@blocknote/xl-multi-column" "0.42.3"
"@blocknote/core" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/react" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@009cb604565b1348591048dc374763b4308e3880"
"@blocknote/xl-multi-column" "https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@009cb604565b1348591048dc374763b4308e3880"
"@react-pdf/renderer" "^4.3.0"
buffer "^6.0.3"
docx "^9.5.1"
@@ -5261,20 +5327,20 @@
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149"
integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==
"@tiptap/core@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.11.0.tgz#122a1db7852c9cea48221290210e713bb4efd66e"
integrity sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==
"@tiptap/core@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.10.2.tgz#bcb37bb1239c01158f8ae97e3232bf8740498683"
integrity sha512-rWgo/9g5lSWT3/00wPvG+3EEuPqDxegYMp0v7YkSuURi43Btf+SG4yGtQ5Si9ICF0NJjeZoHLusrjeVltcrsSw==
"@tiptap/extension-bold@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.10.2.tgz#c2160024bcd672bf399ddb97a409c3dc6ba3ceb0"
integrity sha512-lgUpWuBhlZwf+/pVKfqVUpHfA5PDECDyobcXmMrRSpreM+58psZtWDZMZ21K94SmJukRidW7vdNWoTSRSEiY4Q==
"@tiptap/extension-bubble-menu@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.11.0.tgz#2ce7820c9aecd0f4ce36c2668353aa8194ea55a5"
integrity sha512-P3j9lQ+EZ5Zg/isJzLpCPX7bp7WUBmz8GPs/HPlyMyN2su8LqXntITBZr8IP1JNBlB/wR83k/W0XqdC57mG7cA==
"@tiptap/extension-bubble-menu@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.10.2.tgz#cc027da2287cc0da63b13f70e3ab2543fef08034"
integrity sha512-gT4PMDXWdUAdijPH35LDUsPv+YIIhEHUuvqPFBGRudrycQ2TlWMmRZ2jYNg1PGBh+/UHVR2l8TNuZ5QSr88ISQ==
dependencies:
"@floating-ui/dom" "^1.0.0"
@@ -5283,10 +5349,10 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.10.2.tgz#11a057d9a649dc9e914e32fbfb23e7748a1f1d67"
integrity sha512-+oA2fuQPQDzZb3q0pQeObPrhWXPh9JxybnAAGFoGenZsMsoUdN8x/KdtrXGWDMoB9XIg7XwE1xO6EZAH+eLe8Q==
"@tiptap/extension-floating-menu@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.11.0.tgz#521109d9c0d5f6dc5fb6f2fd8181367af8a91be2"
integrity sha512-nEHdWZHEJYX1II1oJQ4aeZ8O/Kss4BRbYFXQFGIvPelCfCYEATpUJh3aq3767ARSq40bOWyu+Dcd4SCW0We6Sw==
"@tiptap/extension-floating-menu@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.10.2.tgz#cd5d17b9b71fec17d3375887cef1636c2c79ea6f"
integrity sha512-B14/MFffhyowF4/OIive8Z/pL0LWxZxehVBMm4eGG09O01s9/oTc2pDkhMUxbR/1ip81Se1/i+KlTbxogcuduA==
"@tiptap/extension-gapcursor@^3.7.2":
version "3.10.2"
@@ -5340,10 +5406,10 @@
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.10.2.tgz#5f5ba24a109894f19acc443c57963c32f60e16f1"
integrity sha512-XyvMn6B6PCPsgV6VMLiS1QXI1OKarBAYwXmqsE+gCzzYyXxYX4sLUlQ8JKysREyIGMHxSg5vgOajsgXgFMrvyA==
"@tiptap/pm@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.11.0.tgz#c9d2bef0db08a5a5b2c6cce035fe893a475ee638"
integrity sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==
"@tiptap/pm@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.10.2.tgz#f156f358821517990f01359e0c0a0782b6bc0b66"
integrity sha512-qXsp7guPLoir49Fh6IOzg6IAJA3tYYy/1316vv7DhJwmdF9GebkwgFcei2XGk6vKlwv18jWV+BlqDv9iwQ5Alg==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
@@ -5364,17 +5430,17 @@
prosemirror-transform "^1.10.2"
prosemirror-view "^1.38.1"
"@tiptap/react@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.11.0.tgz#b9dd344101cd64df45cb7a5785f98c7d3a689f72"
integrity sha512-SDGei/2DjwmhzsxIQNr6dkB6NxLgXZjQ6hF36NfDm4937r5NLrWrNk5tCsoDQiKZ0DHEzuJ6yZM5C7I7LZLB6w==
"@tiptap/react@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.10.2.tgz#3d4ed9a0e8e3c02d93e2735078730f2a0e61fb3a"
integrity sha512-3pvtpG0Wuy4iozYnCFrf3y0PPxOZ1oe/T2DNNTLelui4CiphS3sQoBtuVrzx+2QePGkFM5uU5Bj+ET3NmUbwWA==
dependencies:
"@types/use-sync-external-store" "^0.0.6"
fast-deep-equal "^3.1.3"
use-sync-external-store "^1.4.0"
optionalDependencies:
"@tiptap/extension-bubble-menu" "^3.11.0"
"@tiptap/extension-floating-menu" "^3.11.0"
"@tiptap/extension-bubble-menu" "^3.10.2"
"@tiptap/extension-floating-menu" "^3.10.2"
"@trysound/sax@0.2.0":
version "0.2.0"
@@ -12197,7 +12263,7 @@ prosemirror-trailing-node@^3.0.0:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@1.10.4, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
prosemirror-transform@1.10.4, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.10.4, prosemirror-transform@^1.7.3:
version "1.10.4"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz#56419eac14f9f56612c806ae46f9238648f3f02e"
integrity sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==

1
src/helm/helmfile.gotmpl Symbolic link
View File

@@ -0,0 +1 @@
helmfile.yaml

View File

@@ -1,10 +1,10 @@
environments:
dev:
values:
- version: 3.10.0
- version: 3.9.0
feature:
values:
- version: 3.10.0
- version: 3.9.0
feature: ci
domain: example.com
imageTag: demo

View File

@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.10.0
version: 3.9.0
appVersion: latest

View File

@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.10.0",
"version": "3.9.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {