mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-07 15:43:01 +02:00
Compare commits
8 Commits
fix-warnin
...
bug/fix-fl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed8062ea52 | ||
|
|
de8dea20d5 | ||
|
|
342fc2ab59 | ||
|
|
b8132ef393 | ||
|
|
2ede746d8a | ||
|
|
5bd0764bdd | ||
|
|
610948cd16 | ||
|
|
96bb99d6ec |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0] - 2025-01-13
|
||||
|
||||
## Added
|
||||
|
||||
- 🔧(backend) add option to configure list of essential OIDC claims #525 & #531
|
||||
@@ -25,10 +27,15 @@ and this project adheres to
|
||||
- 💄(frontend) updating the header and leftpanel for responsive #421
|
||||
- 💄(frontend) update DocsGrid component #431
|
||||
- 💄(frontend) update DocsGridOptions component #432
|
||||
- 💄(frontend) update DocHeader ui #446
|
||||
- 💄(frontend) update DocHeader ui #448
|
||||
- 💄(frontend) update doc versioning ui #463
|
||||
- 💄(frontend) update doc summary ui #473
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) fix create document via s2s if sub unknown but email found #543
|
||||
- 🐛(frontend) hide search and create doc button if not authenticated #555
|
||||
- 🐛(backend) race condition creation issue #556
|
||||
|
||||
## [1.10.0] - 2024-12-17
|
||||
|
||||
@@ -197,7 +204,7 @@ and this project adheres to
|
||||
|
||||
- 🛂(frontend) match email if no existing user matches the sub
|
||||
- 🐛(backend) gitlab oicd userinfo endpoint #232
|
||||
- 🛂(frontend) redirect to the OIDC when private doc and unauthenticated #292
|
||||
- 🛂(frontend) redirect to the OIDC when private doc and unauthentified #292
|
||||
- ♻️(backend) getting list of document versions available for a user #258
|
||||
- 🔧(backend) fix configuration to avoid different ssl warning #297
|
||||
- 🐛(frontend) fix editor break line not working #302
|
||||
@@ -326,7 +333,7 @@ and this project adheres to
|
||||
- ⚡️(e2e) unique login between tests (#80)
|
||||
- ⚡️(CI) improve e2e job (#86)
|
||||
- ♻️(frontend) improve the error and message info ui (#93)
|
||||
- ✏️(frontend) change all occurrences of pad to doc (#99)
|
||||
- ✏️(frontend) change all occurences of pad to doc (#99)
|
||||
|
||||
## Fixed
|
||||
|
||||
@@ -346,7 +353,8 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.10.0...main
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.0.0...main
|
||||
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
|
||||
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
|
||||
[v1.9.0]: https://github.com/numerique-gouv/impress/releases/v1.9.0
|
||||
[v1.8.2]: https://github.com/numerique-gouv/impress/releases/v1.8.2
|
||||
|
||||
@@ -7,7 +7,7 @@ UNSET_USER=0
|
||||
|
||||
TERRAFORM_DIRECTORY="./env.d/terraform"
|
||||
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
|
||||
COMPOSE_PROJECT="impress"
|
||||
COMPOSE_PROJECT="docs"
|
||||
|
||||
|
||||
# _set_user: set (or unset) default user id used to run docker commands
|
||||
|
||||
@@ -6,7 +6,7 @@ Whenever we are cooking a new release (e.g. `4.18.1`) we should follow a standar
|
||||
2. Bump the release number for backend project, frontend projects, and Helm files:
|
||||
|
||||
- for backend, update the version number by hand in `pyproject.toml`,
|
||||
- for each project (`src/frontend`, `src/frontend/apps/*`, `src/frontend/packages/*`, `src/mail`), run `yarn version --new-version --no-git-tag-version 4.18.1` in their directory. This will update their `package.json` for you,
|
||||
- for each projects (`src/frontend`, `src/frontend/apps/*`, `src/frontend/packages/*`, `src/mail`), run `yarn version --new-version --no-git-tag-version 4.18.1` in their directory. This will update their `package.json` for you,
|
||||
- for Helm, update Docker image tag in files located at `src/helm/env.d` for both `preprod` and `production` environments:
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -201,7 +201,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"abilities",
|
||||
"created_at",
|
||||
"creator",
|
||||
"is_avorite",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
"nb_accesses",
|
||||
@@ -264,13 +264,17 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
"""Create the document and associate it with the user or send an invitation."""
|
||||
language = validated_data.get("language", settings.LANGUAGE_CODE)
|
||||
|
||||
# Get the user based on the sub (unique identifier)
|
||||
# Get the user on its sub (unique identifier). Default on email if allowed in settings
|
||||
email = validated_data["email"]
|
||||
|
||||
try:
|
||||
user = models.User.objects.get(sub=validated_data["sub"])
|
||||
except (models.User.DoesNotExist, KeyError):
|
||||
user = None
|
||||
email = validated_data["email"]
|
||||
else:
|
||||
user = models.User.objects.get_user_by_sub_or_email(
|
||||
validated_data["sub"], email
|
||||
)
|
||||
except models.DuplicateEmailError as err:
|
||||
raise serializers.ValidationError({"email": [err.message]}) from err
|
||||
|
||||
if user:
|
||||
email = user.email
|
||||
language = user.language or language
|
||||
|
||||
@@ -279,7 +283,9 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
validated_data["content"]
|
||||
)
|
||||
except ConversionError as err:
|
||||
raise exceptions.APIException(detail="could not convert content") from err
|
||||
raise serializers.ValidationError(
|
||||
{"content": ["Could not convert content"]}
|
||||
) from err
|
||||
|
||||
document = models.Document.objects.create(
|
||||
title=validated_data["title"],
|
||||
@@ -302,7 +308,11 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
|
||||
# Notify the user about the newly created document
|
||||
self._send_email_notification(document, validated_data, email, language)
|
||||
return document
|
||||
|
||||
def _send_email_notification(self, document, validated_data, email, language):
|
||||
"""Notify the user about the newly created document."""
|
||||
subject = validated_data.get("subject") or _(
|
||||
"A new document was created on your behalf!"
|
||||
)
|
||||
@@ -313,8 +323,6 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
}
|
||||
document.send_email(subject, [email], context, language)
|
||||
|
||||
return document
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
This serializer does not support updates.
|
||||
|
||||
@@ -140,7 +140,6 @@ class UserViewSet(
|
||||
permission_classes = [permissions.IsSelf]
|
||||
queryset = models.User.objects.all()
|
||||
serializer_class = serializers.UserSerializer
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
@@ -630,7 +629,7 @@ class DocumentViewSet(
|
||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||
is configured to do this.
|
||||
|
||||
Based on the original url and the logged-in user, we must decide if we authorize Nginx
|
||||
Based on the original url and the logged in user, we must decide if we authorize Nginx
|
||||
to let this request go through (by returning a 200 code) or if we block it (by returning
|
||||
a 403 error). Note that we return 403 errors without any further details for security
|
||||
reasons.
|
||||
@@ -677,7 +676,7 @@ class DocumentViewSet(
|
||||
|
||||
# Fetch the document and check if the user has access
|
||||
try:
|
||||
document, _created = models.Document.objects.get_or_create(pk=pk)
|
||||
document = models.Document.objects.get(pk=pk)
|
||||
except models.Document.DoesNotExist as exc:
|
||||
logger.debug("Document with ID '%s' does not exist", pk)
|
||||
raise drf.exceptions.PermissionDenied() from exc
|
||||
@@ -835,7 +834,7 @@ class DocumentAccessViewSet(
|
||||
serializer_class = serializers.DocumentAccessSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Add new access to the document and email the new added user."""
|
||||
"""Add a new access to the document and send an email to the new added user."""
|
||||
access = serializer.save()
|
||||
language = self.request.headers.get("Content-Language", "en-us")
|
||||
|
||||
@@ -847,7 +846,7 @@ class DocumentAccessViewSet(
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Update access to the document and notify the collaboration server."""
|
||||
"""Update an access to the document and notify the collaboration server."""
|
||||
access = serializer.save()
|
||||
|
||||
access_user_id = None
|
||||
@@ -860,7 +859,7 @@ class DocumentAccessViewSet(
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Delete access to the document and notify the collaboration server."""
|
||||
"""Delete an access to the document and notify the collaboration server."""
|
||||
instance.delete()
|
||||
|
||||
# Notify collaboration server about the access removed
|
||||
@@ -1099,7 +1098,7 @@ class InvitationViewset(
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Save invitation to a document then email the invited user."""
|
||||
"""Save invitation to a document then send an email to the invited user."""
|
||||
invitation = serializer.save()
|
||||
|
||||
language = self.request.headers.get("Content-Language", "en-us")
|
||||
|
||||
@@ -11,7 +11,7 @@ from mozilla_django_oidc.auth import (
|
||||
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
||||
)
|
||||
|
||||
from core.models import User
|
||||
from core.models import DuplicateEmailError, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -98,7 +98,10 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
"short_name": short_name,
|
||||
}
|
||||
|
||||
user = self.get_existing_user(sub, email)
|
||||
try:
|
||||
user = User.objects.get_user_by_sub_or_email(sub, email)
|
||||
except DuplicateEmailError as err:
|
||||
raise SuspiciousOperation(err.message) from err
|
||||
|
||||
if user:
|
||||
if not user.is_active:
|
||||
@@ -117,16 +120,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
)
|
||||
return full_name or None
|
||||
|
||||
def get_existing_user(self, sub, email):
|
||||
"""Fetch an existing user by sub (or email as a fallback respecting fallback setting."""
|
||||
try:
|
||||
return User.objects.get(sub=sub)
|
||||
except User.DoesNotExist:
|
||||
if email and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
|
||||
return User.objects.filter(email=email).first()
|
||||
|
||||
return None
|
||||
|
||||
def update_user_if_needed(self, user, claims):
|
||||
"""Update user claims if they have changed."""
|
||||
has_changed = any(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from mozilla_django_oidc.urls import urlpatterns as mozilla_oidc_urls
|
||||
from mozilla_django_oidc.urls import urlpatterns as mozzila_oidc_urls
|
||||
|
||||
from .views import OIDCLogoutCallbackView, OIDCLogoutView
|
||||
|
||||
@@ -14,5 +14,5 @@ urlpatterns = [
|
||||
OIDCLogoutCallbackView.as_view(),
|
||||
name="oidc_logout_callback",
|
||||
),
|
||||
*mozilla_oidc_urls,
|
||||
*mozzila_oidc_urls,
|
||||
]
|
||||
|
||||
@@ -19,7 +19,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
skip_postgeneration_save = True
|
||||
|
||||
sub = factory.Sequence(lambda n: f"user{n!s}")
|
||||
email = factory.Faker("email")
|
||||
@@ -37,8 +36,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
if create and (extracted is True):
|
||||
UserDocumentAccessFactory(user=self, role="owner")
|
||||
|
||||
self.save()
|
||||
|
||||
@factory.post_generation
|
||||
def with_owned_template(self, create, extracted, **kwargs):
|
||||
"""
|
||||
@@ -48,8 +45,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
if create and (extracted is True):
|
||||
UserTemplateAccessFactory(user=self, role="owner")
|
||||
|
||||
self.save()
|
||||
|
||||
|
||||
class DocumentFactory(factory.django.DjangoModelFactory):
|
||||
"""A factory to create documents"""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Generated by Django 5.0.3 on 2024-05-28 20:29
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import timezone_field.fields
|
||||
import uuid
|
||||
@@ -143,7 +145,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='documentaccess',
|
||||
constraint=models.CheckConstraint(condition=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='invitation',
|
||||
@@ -159,6 +161,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='templateaccess',
|
||||
constraint=models.CheckConstraint(condition=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-13 22:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_make_document_creator_and_invitation_issuer_optional'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='user',
|
||||
options={'ordering': ('-created_at',), 'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Declare and configure the models for the impress core application
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import hashlib
|
||||
import smtplib
|
||||
@@ -89,6 +90,16 @@ class LinkReachChoices(models.TextChoices):
|
||||
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||
|
||||
|
||||
class DuplicateEmailError(Exception):
|
||||
"""Raised when an email is already associated with a pre-existing user."""
|
||||
|
||||
def __init__(self, message=None, email=None):
|
||||
"""Set message and email to describe the exception."""
|
||||
self.message = message
|
||||
self.email = email
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
"""
|
||||
Serves as an abstract base model for other models, ensuring that records are validated
|
||||
@@ -126,6 +137,35 @@ class BaseModel(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class UserManager(auth_models.UserManager):
|
||||
"""Custom manager for User model with additional methods."""
|
||||
|
||||
def get_user_by_sub_or_email(self, sub, email):
|
||||
"""Fetch existing user by sub or email."""
|
||||
try:
|
||||
return self.get(sub=sub)
|
||||
except self.model.DoesNotExist as err:
|
||||
if not email:
|
||||
return None
|
||||
|
||||
if settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
|
||||
try:
|
||||
return self.get(email=email)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
elif (
|
||||
self.filter(email=email).exists()
|
||||
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
|
||||
):
|
||||
raise DuplicateEmailError(
|
||||
_(
|
||||
"We couldn't find a user with this sub but the email is already "
|
||||
"associated with a registered user."
|
||||
)
|
||||
) from err
|
||||
return None
|
||||
|
||||
|
||||
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
"""User model to work with OIDC only authentication."""
|
||||
|
||||
@@ -155,7 +195,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
||||
|
||||
# Unlike the "email" field which stores the email coming from the OIDC token, this field
|
||||
# stores the email used by staff users to log in to the admin site
|
||||
# stores the email used by staff users to login to the admin site
|
||||
admin_email = models.EmailField(
|
||||
_("admin email address"), unique=True, blank=True, null=True
|
||||
)
|
||||
@@ -192,14 +232,13 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
),
|
||||
)
|
||||
|
||||
objects = auth_models.UserManager()
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = "admin_email"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
db_table = "impress_user"
|
||||
ordering = ("-created_at",)
|
||||
verbose_name = _("user")
|
||||
verbose_name_plural = _("users")
|
||||
|
||||
@@ -696,7 +735,7 @@ class DocumentAccess(BaseAccess):
|
||||
violation_error_message=_("This team is already in this document."),
|
||||
),
|
||||
models.CheckConstraint(
|
||||
condition=models.Q(user__isnull=False, team="")
|
||||
check=models.Q(user__isnull=False, team="")
|
||||
| models.Q(user__isnull=True, team__gt=""),
|
||||
name="check_document_access_either_user_or_team",
|
||||
violation_error_message=_("Either user or team must be set, not both."),
|
||||
@@ -761,7 +800,7 @@ class Template(BaseModel):
|
||||
"""
|
||||
document_html = weasyprint.HTML(
|
||||
string=DjangoTemplate(self.code).render(
|
||||
Context({"body": html.format_html("{}", body_html), **metadata})
|
||||
Context({"body": html.format_html(body_html), **metadata})
|
||||
)
|
||||
)
|
||||
css = weasyprint.CSS(
|
||||
@@ -780,7 +819,7 @@ class Template(BaseModel):
|
||||
Generate and return a docx document wrapped around the current template
|
||||
"""
|
||||
template_string = DjangoTemplate(self.code).render(
|
||||
Context({"body": html.format_html("{}", body_html), **metadata})
|
||||
Context({"body": html.format_html(body_html), **metadata})
|
||||
)
|
||||
|
||||
html_string = f"""
|
||||
@@ -798,6 +837,7 @@ class Template(BaseModel):
|
||||
"""
|
||||
|
||||
reference_docx = "core/static/reference.docx"
|
||||
output = BytesIO()
|
||||
|
||||
# Convert the HTML to a temporary docx file
|
||||
with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file:
|
||||
@@ -884,7 +924,7 @@ class TemplateAccess(BaseAccess):
|
||||
violation_error_message=_("This team is already in this template."),
|
||||
),
|
||||
models.CheckConstraint(
|
||||
condition=models.Q(user__isnull=False, team="")
|
||||
check=models.Q(user__isnull=False, team="")
|
||||
| models.Q(user__isnull=True, team__gt=""),
|
||||
name="check_template_access_either_user_or_team",
|
||||
violation_error_message=_("Either user or team must be set, not both."),
|
||||
@@ -939,7 +979,10 @@ class Invitation(BaseModel):
|
||||
super().clean()
|
||||
|
||||
# Check if an identity already exists for the provided email
|
||||
if User.objects.filter(email=self.email).exists():
|
||||
if (
|
||||
User.objects.filter(email=self.email).exists()
|
||||
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
|
||||
):
|
||||
raise exceptions.ValidationError(
|
||||
{"email": _("This email is already associated to a registered user.")}
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ class CollaborationService:
|
||||
def reset_connections(self, room, user_id=None):
|
||||
"""
|
||||
Reset connections of a room in the collaboration server.
|
||||
Resetting a connection means that the user will be disconnected and will
|
||||
Reseting a connection means that the user will be disconnected and will
|
||||
have to reconnect to the collaboration server, with updated rights.
|
||||
"""
|
||||
endpoint = "reset-connections"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Unit tests for the Authentication Backends."""
|
||||
|
||||
import random
|
||||
import re
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
@@ -64,7 +65,33 @@ def test_authentication_getter_existing_user_via_email(
|
||||
assert user == db_user
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_fallback_to_email(
|
||||
def test_authentication_getter_email_none(monkeypatch):
|
||||
"""
|
||||
If no user is found with the sub and no email is provided, a new user should be created.
|
||||
"""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
db_user = UserFactory(email=None)
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
user_info = {"sub": "123"}
|
||||
if random.choice([True, False]):
|
||||
user_info["email"] = None
|
||||
return user_info
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
# Since the sub and email didn't match, it should create a new user
|
||||
assert models.User.objects.count() == 2
|
||||
assert user != db_user
|
||||
assert user.sub == "123"
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_fallback_to_email_allow_duplicate(
|
||||
settings, monkeypatch
|
||||
):
|
||||
"""
|
||||
@@ -77,6 +104,7 @@ def test_authentication_getter_existing_user_no_fallback_to_email(
|
||||
|
||||
# Set the setting to False
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
|
||||
settings.OIDC_ALLOW_DUPLICATE_EMAILS = True
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": db_user.email}
|
||||
@@ -93,6 +121,39 @@ def test_authentication_getter_existing_user_no_fallback_to_email(
|
||||
assert user.sub == "123"
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_fallback_to_email_no_duplicate(
|
||||
settings, monkeypatch
|
||||
):
|
||||
"""
|
||||
When the "OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION" setting is set to False,
|
||||
the system should not match users by email, even if the email matches.
|
||||
"""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
db_user = UserFactory()
|
||||
|
||||
# Set the setting to False
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
|
||||
settings.OIDC_ALLOW_DUPLICATE_EMAILS = False
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": db_user.email}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match=(
|
||||
"We couldn't find a user with this sub but the email is already associated "
|
||||
"with a registered user."
|
||||
),
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
# Since the sub doesn't match, it should not create a new user
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_with_email(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
|
||||
@@ -698,7 +698,7 @@ def test_api_document_accesses_delete_administrators_except_owners(
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
):
|
||||
"""
|
||||
Users who are administrators in a document should be allowed to delete access
|
||||
Users who are administrators in a document should be allowed to delete an access
|
||||
from the document provided it is not ownership.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -285,7 +285,7 @@ def test_api_document_versions_retrieve_authenticated_related(via, mock_user_tea
|
||||
assert response.status_code == 404
|
||||
|
||||
# Create a new version should not make it available to the user because
|
||||
# only the current version is available to the user, but it is excluded
|
||||
# only the current version is available to the user but it is excluded
|
||||
# from the list
|
||||
document.content = "new content 1"
|
||||
document.save()
|
||||
|
||||
@@ -134,7 +134,7 @@ def test_api_documents_ai_transform_authenticated_forbidden(reach, role):
|
||||
@patch("openai.resources.chat.completions.Completions.create")
|
||||
def test_api_documents_ai_transform_authenticated_success(mock_create, reach, role):
|
||||
"""
|
||||
Authenticated who are not related to a document should be able to request AI transform
|
||||
Autenticated who are not related to a document should be able to request AI transform
|
||||
if the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -154,7 +154,7 @@ def test_api_documents_ai_translate_authenticated_forbidden(reach, role):
|
||||
@patch("openai.resources.chat.completions.Completions.create")
|
||||
def test_api_documents_ai_translate_authenticated_success(mock_create, reach, role):
|
||||
"""
|
||||
Authenticated who are not related to a document should be able to request AI translate
|
||||
Autenticated who are not related to a document should be able to request AI translate
|
||||
if the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
||||
)
|
||||
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
"""
|
||||
Authenticated who are not related to a document should be able to upload a file
|
||||
Autenticated who are not related to a document should be able to upload a file
|
||||
if the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
@@ -225,7 +225,7 @@ def test_api_documents_attachment_upload_invalid(client):
|
||||
|
||||
|
||||
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
||||
"""The uploaded file should not exceed the maximum size in settings."""
|
||||
"""The uploaded file should not exceeed the maximum size in settings."""
|
||||
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -13,6 +13,7 @@ import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.api.serializers import ServerCreateDocumentSerializer
|
||||
from core.models import Document, Invitation, User
|
||||
from core.services.converter_services import ConversionError, YdocConverter
|
||||
|
||||
@@ -20,7 +21,7 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_convert_markdown():
|
||||
def mock_convert_md():
|
||||
"""Mock YdocConverter.convert_markdown to return a converted content."""
|
||||
with patch.object(
|
||||
YdocConverter,
|
||||
@@ -169,8 +170,11 @@ def test_api_documents_create_for_owner_invalid_sub():
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_existing(mock_convert_markdown):
|
||||
"""It should be possible to create a document on behalf of a pre-existing user."""
|
||||
def test_api_documents_create_for_owner_existing(mock_convert_md):
|
||||
"""
|
||||
It should be possible to create a document on behalf of a pre-existing user
|
||||
by passing their sub and email.
|
||||
"""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
data = {
|
||||
@@ -189,7 +193,7 @@ def test_api_documents_create_for_owner_existing(mock_convert_markdown):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -213,10 +217,10 @@ def test_api_documents_create_for_owner_existing(mock_convert_markdown):
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
|
||||
def test_api_documents_create_for_owner_new_user(mock_convert_md):
|
||||
"""
|
||||
It should be possible to create a document on behalf of new users by
|
||||
passing only their email address.
|
||||
passing their unknown sub and email address.
|
||||
"""
|
||||
data = {
|
||||
"title": "My Document",
|
||||
@@ -234,7 +238,7 @@ def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -264,8 +268,190 @@ def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
|
||||
assert document.creator == user
|
||||
|
||||
|
||||
@override_settings(
|
||||
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=True,
|
||||
)
|
||||
def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback(
|
||||
mock_convert_md,
|
||||
):
|
||||
"""
|
||||
It should be possible to create a document on behalf of a pre-existing user for
|
||||
who the sub was not found if the settings allow it. This edge case should not
|
||||
happen in a healthy OIDC federation but can be usefull if an OIDC provider modifies
|
||||
users sub on each login for example...
|
||||
"""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
|
||||
assert document.title == "My Document"
|
||||
assert document.content == "Converted document content"
|
||||
assert document.creator == user
|
||||
assert document.accesses.filter(user=user, role="owner").exists()
|
||||
|
||||
assert Invitation.objects.exists() is False
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [user.email]
|
||||
assert email.subject == "A new document was created on your behalf!"
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "A new document was created on your behalf!" in email_content
|
||||
assert (
|
||||
"You have been granted ownership of a new document: My Document"
|
||||
) in email_content
|
||||
|
||||
|
||||
@override_settings(
|
||||
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=False,
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS=False,
|
||||
)
|
||||
def test_api_documents_create_for_owner_existing_user_email_no_sub_no_fallback(
|
||||
mock_convert_md,
|
||||
):
|
||||
"""
|
||||
When a user does not match an existing sub and fallback to matching on email is
|
||||
not allowed in settings, it should raise an error if the email is already used by
|
||||
a registered user and duplicate emails are not allowed.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"email": [
|
||||
(
|
||||
"We couldn't find a user with this sub but the email is already "
|
||||
"associated with a registered user."
|
||||
)
|
||||
]
|
||||
}
|
||||
assert mock_convert_md.called is False
|
||||
assert Document.objects.exists() is False
|
||||
assert Invitation.objects.exists() is False
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
||||
@override_settings(
|
||||
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=False,
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS=True,
|
||||
)
|
||||
def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplicate(
|
||||
mock_convert_md,
|
||||
):
|
||||
"""
|
||||
When a user does not match an existing sub and fallback to matching on email is
|
||||
not allowed in settings, it should be possible to create a new user with the same
|
||||
email as an existing user if the settings allow it (identification is still done
|
||||
via the sub in this case).
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
|
||||
assert document.title == "My Document"
|
||||
assert document.content == "Converted document content"
|
||||
assert document.creator is None
|
||||
assert document.accesses.exists() is False
|
||||
|
||||
invitation = Invitation.objects.get()
|
||||
assert invitation.email == user.email
|
||||
assert invitation.role == "owner"
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [user.email]
|
||||
assert email.subject == "A new document was created on your behalf!"
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "A new document was created on your behalf!" in email_content
|
||||
assert (
|
||||
"You have been granted ownership of a new document: My Document"
|
||||
) in email_content
|
||||
|
||||
# The creator field on the document should be set when the user is created
|
||||
user = User.objects.create(email=user.email, password="!")
|
||||
document.refresh_from_db()
|
||||
assert document.creator == user
|
||||
|
||||
|
||||
@patch.object(ServerCreateDocumentSerializer, "_send_email_notification")
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"], LANGUAGE_CODE="de-de")
|
||||
def test_api_documents_create_for_owner_with_default_language(
|
||||
mock_send, mock_convert_md
|
||||
):
|
||||
"""The default language from settings should apply by default."""
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": "john.doe@example.com",
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
assert mock_send.call_args[0][3] == "de-de"
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdown):
|
||||
def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
|
||||
"""
|
||||
Test creating a document with a specific language.
|
||||
Useful if the remote server knows the user's language.
|
||||
@@ -287,7 +473,7 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdo
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -302,7 +488,7 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdo
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
mock_convert_markdown,
|
||||
mock_convert_md,
|
||||
):
|
||||
"""It should be possible to customize the subject and message of the invitation email."""
|
||||
data = {
|
||||
@@ -323,7 +509,7 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -336,11 +522,11 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_converter_exception(
|
||||
mock_convert_markdown,
|
||||
mock_convert_md,
|
||||
):
|
||||
"""It should be possible to customize the subject and message of the invitation email."""
|
||||
"""In case of converter error, a 400 error should be raised."""
|
||||
|
||||
mock_convert_markdown.side_effect = ConversionError("Conversion failed")
|
||||
mock_convert_md.side_effect = ConversionError("Conversion failed")
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
@@ -357,8 +543,33 @@ def test_api_documents_create_for_owner_with_converter_exception(
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"content": ["Could not convert content"]}
|
||||
|
||||
assert response.status_code == 500
|
||||
assert response.json() == {"detail": "could not convert content"}
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_empty_content():
|
||||
"""The content should not be empty or a 400 error should be raised."""
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": " ",
|
||||
"sub": "123",
|
||||
"email": "john.doe@example.com",
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"content": [
|
||||
"This field may not be blank.",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ def test_api_documents_media_auth_authenticated_restricted():
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
"""
|
||||
Users who have specific access to a document, whatever the role, should be able to
|
||||
Users who have a specific access to a document, whatever the role, should be able to
|
||||
retrieve related attachments.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -647,7 +647,7 @@ def test_api_template_accesses_delete_administrators_except_owners(
|
||||
via, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Users who are administrators in a template should be allowed to delete access
|
||||
Users who are administrators in a template should be allowed to delete an access
|
||||
from the template provided it is not ownership.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_models_documents_file_key():
|
||||
def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role):
|
||||
"""
|
||||
Check abilities returned for a document giving insufficient roles to link holders
|
||||
i.e. anonymous users or authenticated users who have no specific role on the document.
|
||||
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||
@@ -121,7 +121,7 @@ def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role)
|
||||
def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
||||
"""
|
||||
Check abilities returned for a document giving reader role to link holders
|
||||
i.e. anonymous users or authenticated users who have no specific role on the document.
|
||||
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role="reader")
|
||||
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||
@@ -158,7 +158,7 @@ def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
||||
def test_models_documents_get_abilities_editor(is_authenticated, reach):
|
||||
"""
|
||||
Check abilities returned for a document giving editor role to link holders
|
||||
i.e. anonymous users or authenticated users who have no specific role on the document.
|
||||
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
|
||||
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||
@@ -449,7 +449,7 @@ def test_models_documents__email_invitation__success():
|
||||
|
||||
def test_models_documents__email_invitation__success_fr():
|
||||
"""
|
||||
The email invitation is sent successfully in French.
|
||||
The email invitation is sent successfully in french.
|
||||
"""
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_models_users_id_unique():
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_existing():
|
||||
"""The 'email_user' method should send mail to the user's email address."""
|
||||
"""The "email_user' method should send mail to the user's email address."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
with mock.patch("django.core.mail.send_mail") as mock_send:
|
||||
@@ -37,7 +37,7 @@ def test_models_users_send_mail_main_existing():
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_missing():
|
||||
"""The 'email_user' method should fail if the user has no email address."""
|
||||
"""The "email_user' method should fail if the user has no email address."""
|
||||
user = factories.UserFactory(email=None)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Test AI API endpoints in the impress core app.
|
||||
Test ai API endpoints in the impress core app.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
30
src/backend/core/tests/test_settings.py
Normal file
30
src/backend/core/tests/test_settings.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Unit tests for the User model
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from impress.settings import Base
|
||||
|
||||
|
||||
def test_invalid_settings_oidc_email_configuration():
|
||||
"""
|
||||
The OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and OIDC_ALLOW_DUPLICATE_EMAILS settings
|
||||
should not be both set to True simultaneously.
|
||||
"""
|
||||
|
||||
class TestSettings(Base):
|
||||
"""Fake test settings."""
|
||||
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS = True
|
||||
|
||||
# The validation is performed during post_setup
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
TestSettings().post_setup()
|
||||
|
||||
# Check the exception message
|
||||
assert str(excinfo.value) == (
|
||||
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
|
||||
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
|
||||
)
|
||||
@@ -15,7 +15,7 @@ class Command(BaseCommand):
|
||||
"""Define required arguments "email" and "password"."""
|
||||
parser.add_argument(
|
||||
"--email",
|
||||
help="Email for the user.",
|
||||
help=("Email for the user."),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password",
|
||||
|
||||
@@ -474,6 +474,15 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# WARNING: Enabling this setting allows multiple user accounts to share the same email
|
||||
# address. This may cause security issues and is not recommended for production use when
|
||||
# email is activated as fallback for identification (see previous setting).
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="OIDC_ALLOW_DUPLICATE_EMAILS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue(
|
||||
default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None
|
||||
)
|
||||
@@ -622,9 +631,17 @@ class Base(Configuration):
|
||||
release=get_release(),
|
||||
integrations=[DjangoIntegration()],
|
||||
)
|
||||
# Add the application name to the Sentry scope
|
||||
scope = sentry_sdk.get_global_scope()
|
||||
scope.set_tag("application", "backend")
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
scope.set_extra("application", "backend")
|
||||
|
||||
if (
|
||||
cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
|
||||
and cls.OIDC_ALLOW_DUPLICATE_EMAILS
|
||||
):
|
||||
raise ValueError(
|
||||
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
|
||||
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
|
||||
)
|
||||
|
||||
|
||||
class Build(Base):
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-12-17 15:50+0000\n"
|
||||
"PO-Revision-Date: 2024-12-17 15:53\n"
|
||||
"PO-Revision-Date: 2025-01-14 15:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -31,23 +31,23 @@ msgstr "Wichtige Daten"
|
||||
|
||||
#: core/api/filters.py:16
|
||||
msgid "Creator is me"
|
||||
msgstr ""
|
||||
msgstr "Ersteller bin ich"
|
||||
|
||||
#: core/api/filters.py:19
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
msgstr "Favorit"
|
||||
|
||||
#: core/api/filters.py:22
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "Titel"
|
||||
|
||||
#: core/api/serializers.py:307
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||
|
||||
#: core/api/serializers.py:311
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||
|
||||
#: core/api/serializers.py:414
|
||||
msgid "Body"
|
||||
@@ -63,15 +63,15 @@ msgstr "Format"
|
||||
|
||||
#: core/authentication/backends.py:57
|
||||
msgid "Invalid response format or token verification failed"
|
||||
msgstr ""
|
||||
msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen"
|
||||
|
||||
#: core/authentication/backends.py:81
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr ""
|
||||
msgstr "Benutzerinfo enthielt keine erkennbare Benutzeridentifikation"
|
||||
|
||||
#: core/authentication/backends.py:88
|
||||
msgid "User account is disabled"
|
||||
msgstr ""
|
||||
msgstr "Benutzerkonto ist deaktiviert"
|
||||
|
||||
#: core/models.py:62 core/models.py:69
|
||||
msgid "Reader"
|
||||
@@ -127,31 +127,31 @@ msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
|
||||
|
||||
#: core/models.py:135
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
|
||||
|
||||
#: core/models.py:141
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
msgstr "unter"
|
||||
|
||||
#: core/models.py:143
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
|
||||
|
||||
#: core/models.py:152
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
msgstr "Name"
|
||||
|
||||
#: core/models.py:153
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
msgstr "Kurzbezeichnung"
|
||||
|
||||
#: core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
msgstr "Admin E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:167
|
||||
msgid "language"
|
||||
@@ -159,35 +159,35 @@ msgstr "Sprache"
|
||||
|
||||
#: core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
|
||||
|
||||
#: core/models.py:174
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
|
||||
|
||||
#: core/models.py:177
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
msgstr "Gerät"
|
||||
|
||||
#: core/models.py:179
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
|
||||
|
||||
#: core/models.py:182
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
msgstr "Status des Teammitgliedes"
|
||||
|
||||
#: core/models.py:184
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
|
||||
|
||||
#: core/models.py:187
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
msgstr "aktiviert"
|
||||
|
||||
#: core/models.py:190
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
|
||||
|
||||
#: core/models.py:202
|
||||
msgid "user"
|
||||
@@ -216,25 +216,25 @@ msgstr "Unbenanntes Dokument"
|
||||
#: core/models.py:593
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: core/models.py:597
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
||||
|
||||
#: core/models.py:600
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
|
||||
|
||||
#: core/models.py:623
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: core/models.py:624
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: core/models.py:630
|
||||
msgid "A link trace already exists for this document/user."
|
||||
@@ -242,23 +242,23 @@ msgstr ""
|
||||
|
||||
#: core/models.py:653
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: core/models.py:654
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: core/models.py:660
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
|
||||
|
||||
#: core/models.py:682
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: core/models.py:683
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: core/models.py:689
|
||||
msgid "This user is already in this document."
|
||||
@@ -294,101 +294,101 @@ msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: core/models.py:731
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: core/models.py:732
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: core/models.py:871
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: core/models.py:872
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: core/models.py:878
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: core/models.py:884
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: core/models.py:907
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:926
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: core/models.py:927
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: core/models.py:944
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
|
||||
msgid "Company logo"
|
||||
msgstr ""
|
||||
msgstr "Unternehmens-Logo"
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
#, python-format
|
||||
msgid "Hello %(name)s"
|
||||
msgstr ""
|
||||
msgstr "Guten Tag %(name)s!"
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
msgstr "Hallo"
|
||||
|
||||
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
|
||||
msgid "Thank you very much for your visit!"
|
||||
msgstr ""
|
||||
msgstr "Vielen Dank für Ihren Besuch!"
|
||||
|
||||
#: core/templates/mail/html/hello.html:221
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||
msgstr ""
|
||||
msgstr "Diese E-Mail wurde an %(email)s von <a href=\"%(href)s\">%(name)s</a> gesendet"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
msgstr "Logo-E-Mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
msgstr " Erstellt von %(brandname)s "
|
||||
|
||||
#: core/templates/mail/text/hello.txt:8
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr ""
|
||||
msgstr "Diese E-Mail wurde an %(email)s von %(name)s [%(href)s ] gesendet"
|
||||
|
||||
#: impress/settings.py:236
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
msgstr "Englisch"
|
||||
|
||||
#: impress/settings.py:237
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
msgstr "Französisch"
|
||||
|
||||
#: impress/settings.py:238
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
msgstr "Deutsch"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "1.10.0"
|
||||
version = "2.0.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const addNewMember = async (
|
||||
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
|
||||
fillText: string = 'user',
|
||||
) => {
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/users/?q=${fillText}`) &&
|
||||
@@ -104,7 +106,7 @@ export const goToGridDoc = async (
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(docsGrid.getByTestId('grid-loader')).toBeHidden();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden({ timeout: 10000 });
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
||||
|
||||
// When the visibility is changed, the ws should close the connection (backend signal)
|
||||
// When the visibility is changed, the ws should closed the connection (backend signal)
|
||||
const wsClosePromise = webSocket.waitForEvent('close');
|
||||
|
||||
await selectVisibility.click();
|
||||
|
||||
@@ -160,7 +160,7 @@ test.describe('Document create member', () => {
|
||||
await page.getByRole('button', { name: 'Partager' }).click();
|
||||
|
||||
const inputSearch = page.getByRole('combobox', {
|
||||
name: 'Quick search input',
|
||||
name: 'Saisie de recherche rapide',
|
||||
});
|
||||
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
|
||||
@@ -67,7 +67,7 @@ test.describe('Doc Visibility', () => {
|
||||
test.describe('Doc Visibility: Restricted', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('A doc is not accessible when not authenticated.', async ({
|
||||
test('A doc is not accessible when not authentified.', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
@@ -98,7 +98,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('A doc is not accessible when authenticated but not member.', async ({
|
||||
test('A doc is not accessible when authentified but not member.', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
@@ -232,6 +232,9 @@ test.describe('Doc Visibility: Public', () => {
|
||||
cardContainer.getByText('Public document', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
|
||||
await page
|
||||
@@ -245,6 +248,8 @@ test.describe('Doc Visibility: Public', () => {
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
const card = page.getByLabel('It is the card information');
|
||||
await expect(card).toBeVisible();
|
||||
@@ -316,7 +321,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
test.describe('Doc Visibility: Authenticated', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('A doc is not accessible when unauthenticated.', async ({
|
||||
test('A doc is not accessible when unauthentified.', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
@@ -325,7 +330,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'Authenticated unauthenticated',
|
||||
'Authenticated unauthentified',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
@@ -13,12 +13,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.49.1",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
"typescript": "*",
|
||||
"luxon": "3.5.0",
|
||||
"@types/luxon": "3.4.2"
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"convert-stream": "1.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('fetchAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('check the versioning', () => {
|
||||
it('check the versionning', () => {
|
||||
fetchMock.mock('http://test.jest/api/v2.0/some/url', 200);
|
||||
|
||||
void fetchAPI('some/url', {}, '2.0');
|
||||
|
||||
@@ -33,7 +33,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
|
||||
}, [asPath]);
|
||||
|
||||
// We force to log in except on allowed paths
|
||||
// We force to login except on allowed paths
|
||||
useEffect(() => {
|
||||
if (!initiated || authenticated || pathAllowed) {
|
||||
return;
|
||||
|
||||
@@ -46,8 +46,8 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
terminateCrispSession();
|
||||
window.location.replace(`${baseApiUrl()}logout/`);
|
||||
},
|
||||
// If we try to access a specific page, and we are not authenticated
|
||||
// we store the path in the local storage to redirect to it after log in
|
||||
// If we try to access a specific page and we are not authenticated
|
||||
// we store the path in the local storage to redirect to it after login
|
||||
setAuthUrl() {
|
||||
if (window.location.pathname !== '/') {
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { toBase64 } from '../utils';
|
||||
|
||||
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
const [initialDoc, setInitialDoc] = useState<string>(
|
||||
toBase64(Y.encodeStateAsUpdate(doc)),
|
||||
|
||||
@@ -62,7 +62,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalidQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||
onSuccess(data) {
|
||||
if (data.title !== untitledDocument) {
|
||||
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
||||
|
||||
@@ -94,7 +94,7 @@ const convertToImg = (html: string) => {
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const divs = doc.querySelectorAll('div[data-content-type="image"]');
|
||||
|
||||
// Loop through each div and replace it with an img
|
||||
// Loop through each div and replace it with a img
|
||||
divs.forEach((div) => {
|
||||
const img = document.createElement('img');
|
||||
|
||||
|
||||
@@ -20,18 +20,18 @@ export const createFavoriteDoc = async ({ id }: CreateFavoriteDocParams) => {
|
||||
|
||||
interface CreateFavoriteDocProps {
|
||||
onSuccess?: () => void;
|
||||
listInvalidQueries?: string[];
|
||||
listInvalideQueries?: string[];
|
||||
}
|
||||
|
||||
export function useCreateFavoriteDoc({
|
||||
onSuccess,
|
||||
listInvalidQueries,
|
||||
listInvalideQueries,
|
||||
}: CreateFavoriteDocProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, APIError, CreateFavoriteDocParams>({
|
||||
mutationFn: createFavoriteDoc,
|
||||
onSuccess: () => {
|
||||
listInvalidQueries?.forEach((queryKey) => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
|
||||
@@ -20,18 +20,18 @@ export const deleteFavoriteDoc = async ({ id }: DeleteFavoriteDocParams) => {
|
||||
|
||||
interface DeleteFavoriteDocProps {
|
||||
onSuccess?: () => void;
|
||||
listInvalidQueries?: string[];
|
||||
listInvalideQueries?: string[];
|
||||
}
|
||||
|
||||
export function useDeleteFavoriteDoc({
|
||||
onSuccess,
|
||||
listInvalidQueries,
|
||||
listInvalideQueries,
|
||||
}: DeleteFavoriteDocProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, APIError, DeleteFavoriteDocParams>({
|
||||
mutationFn: deleteFavoriteDoc,
|
||||
onSuccess: () => {
|
||||
listInvalidQueries?.forEach((queryKey) => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
|
||||
@@ -26,18 +26,18 @@ export const updateDoc = async ({
|
||||
|
||||
interface UpdateDocProps {
|
||||
onSuccess?: (data: Doc) => void;
|
||||
listInvalidQueries?: string[];
|
||||
listInvalideQueries?: string[];
|
||||
}
|
||||
|
||||
export function useUpdateDoc({
|
||||
onSuccess,
|
||||
listInvalidQueries,
|
||||
listInvalideQueries,
|
||||
}: UpdateDocProps = {}) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Doc, APIError, UpdateDocParams>({
|
||||
mutationFn: updateDoc,
|
||||
onSuccess: (data) => {
|
||||
listInvalidQueries?.forEach((queryKey) => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
|
||||
@@ -30,12 +30,12 @@ export const updateDocLink = async ({
|
||||
|
||||
interface UpdateDocLinkProps {
|
||||
onSuccess?: (data: Doc) => void;
|
||||
listInvalidQueries?: string[];
|
||||
listInvalideQueries?: string[];
|
||||
}
|
||||
|
||||
export function useUpdateDocLink({
|
||||
onSuccess,
|
||||
listInvalidQueries,
|
||||
listInvalideQueries,
|
||||
}: UpdateDocLinkProps = {}) {
|
||||
const queryClient = useQueryClient();
|
||||
const { broadcast } = useBroadcastStore();
|
||||
@@ -43,7 +43,7 @@ export function useUpdateDocLink({
|
||||
return useMutation<Doc, APIError, UpdateDocLinkParams>({
|
||||
mutationFn: updateDocLink,
|
||||
onSuccess: (data, variable) => {
|
||||
listInvalidQueries?.forEach((queryKey) => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
count === 1
|
||||
? t('Document owner')
|
||||
: t('Share with {{count}} users', {
|
||||
count: count,
|
||||
count,
|
||||
}),
|
||||
elements: members,
|
||||
endActions: membersQuery.hasNextPage
|
||||
@@ -137,7 +137,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
};
|
||||
|
||||
return {
|
||||
groupName: t('Search user result', { count: users.length }),
|
||||
groupName: t('Search user result'),
|
||||
elements: users,
|
||||
endActions:
|
||||
isEmail && users.length === 0
|
||||
|
||||
@@ -49,7 +49,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
},
|
||||
);
|
||||
},
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
|
||||
const updateReach = (link_reach: LinkReach) => {
|
||||
|
||||
@@ -29,10 +29,10 @@ export const useTranslatedShareSettings = () => {
|
||||
icon: 'corporate_fare',
|
||||
value: LinkReach.AUTHENTICATED,
|
||||
descriptionReadOnly: t(
|
||||
'Anyone with the link can see the document provided they are logged in',
|
||||
'Anyone with the link can view the document if they are logged in',
|
||||
),
|
||||
descriptionEdit: t(
|
||||
'Anyone with the link can edit provided they are logged in',
|
||||
'Anyone with the link can edit the document if they are logged in',
|
||||
),
|
||||
},
|
||||
[LinkReach.PUBLIC]: {
|
||||
|
||||
@@ -44,7 +44,7 @@ export const Heading = ({
|
||||
onMouseOver={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
onClick={() => {
|
||||
// With mobile the focus open the keyboard and the scroll are not working
|
||||
// With mobile the focus open the keyboard and the scroll is not working
|
||||
if (!isMobile) {
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ModalConfirmationVersion = ({
|
||||
const { push } = useRouter();
|
||||
const { provider } = useProviderStore();
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
onSuccess: () => {
|
||||
const onDisplaySuccess = () => {
|
||||
toast(t('Version restored successfully'), VariantType.SUCCESS);
|
||||
|
||||
@@ -89,7 +89,7 @@ export const DocsGrid = ({
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{!hasDocs && (
|
||||
{!hasDocs && !loading && (
|
||||
<Box $padding={{ vertical: 'sm' }} $align="center" $justify="center">
|
||||
<Text $size="sm" $variation="600" $weight="700">
|
||||
{t('No documents found')}
|
||||
|
||||
@@ -23,10 +23,10 @@ export const DocsGridActions = ({
|
||||
const deleteModal = useModal();
|
||||
|
||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||
listInvalidQueries: [KEY_LIST_DOC],
|
||||
listInvalideQueries: [KEY_LIST_DOC],
|
||||
});
|
||||
const makeFavoriteDoc = useCreateFavoriteDoc({
|
||||
listInvalidQueries: [KEY_LIST_DOC],
|
||||
listInvalideQueries: [KEY_LIST_DOC],
|
||||
});
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box, Icon, SeparatedSection } from '@/components';
|
||||
import { useAuthStore } from '@/core';
|
||||
import { useCreateDoc } from '@/features/docs/doc-management';
|
||||
import { DocSearchModal } from '@/features/docs/doc-search';
|
||||
import { useCmdK } from '@/hook/useCmdK';
|
||||
@@ -13,6 +14,7 @@ import { useLeftPanelStore } from '../stores';
|
||||
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
const searchModal = useModal();
|
||||
const auth = useAuthStore();
|
||||
useCmdK(searchModal.open);
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
|
||||
@@ -52,16 +54,20 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
<Icon $variation="800" $theme="primary" iconName="house" />
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={searchModal.open}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon $variation="800" $theme="primary" iconName="search" />
|
||||
}
|
||||
/>
|
||||
{auth.authenticated && (
|
||||
<Button
|
||||
onClick={searchModal.open}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon $variation="800" $theme="primary" iconName="search" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Button onClick={createNewDoc}>{t('New doc')}</Button>
|
||||
{auth.authenticated && (
|
||||
<Button onClick={createNewDoc}>{t('New doc')}</Button>
|
||||
)}
|
||||
</Box>
|
||||
</SeparatedSection>
|
||||
{children}
|
||||
|
||||
@@ -112,7 +112,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
||||
};
|
||||
|
||||
/**
|
||||
* When we get a network error.
|
||||
* When we get an network error.
|
||||
*/
|
||||
handlerDidError: WorkboxPlugin['handlerDidError'] = async ({ request }) => {
|
||||
if (!this.isFetchDidFailed) {
|
||||
|
||||
@@ -34,7 +34,7 @@ interface IDocsDB extends DBSchema {
|
||||
type TableName = 'doc-list' | 'doc-item' | 'doc-mutation';
|
||||
|
||||
/**
|
||||
* IndexDB version must be an integer
|
||||
* IndexDB version must be a integer
|
||||
* @returns
|
||||
*/
|
||||
const getCurrentVersion = () => {
|
||||
|
||||
@@ -2,85 +2,145 @@
|
||||
"de": {
|
||||
"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.",
|
||||
"AI Actions": "KI-Aktionen",
|
||||
"AI seems busy! Please try again.": "KI scheint beschäftigt! Bitte versuchen Sie es erneut.",
|
||||
"Accessibility": "Barrierefreiheit",
|
||||
"Accessibility statement": "Erklärung zur Barrierefreiheit",
|
||||
"Add": "Hinzufügen",
|
||||
"Address:": "Anschrift:",
|
||||
"Administrator": "Administrator",
|
||||
"Anyone on the internet with the link can view": "Für jeden im Internet mit diesem Link sichtbar",
|
||||
"All docs": "Alle Dokumente",
|
||||
"Anonymous": "Gast",
|
||||
"Anyone with the link can edit the document": "Jeder mit dem Link kann das Dokument bearbeiten",
|
||||
"Anyone with the link can edit the document if they are logged in": "Jeder mit dem Link kann das Dokument bearbeiten, wenn er angemeldet ist",
|
||||
"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",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Sind Sie sicher, dass Sie das Dokument \"{{title}}\" löschen möchten?",
|
||||
"Back to home page": "Zurück zur Startseite",
|
||||
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Choose a role": "Wählen Sie eine Rolle",
|
||||
"Close the modal": "Pop up schliessen",
|
||||
"Compliance status": "Konformitätsstatus",
|
||||
"Confirm deletion": "Löschung bestätigen",
|
||||
"Connected": "Verbunden",
|
||||
"Content modal to delete document": "Inhalts-Modal zum Löschen des Dokuments",
|
||||
"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",
|
||||
"Delete": "Löschen",
|
||||
"Delete a doc": "Dokument löschen",
|
||||
"Delete document": "Dokument löschen",
|
||||
"Deleting the document \"{{title}}\"": "Lösche das Dokument \"{{title}}\"",
|
||||
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Docs Logo",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Pages: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
|
||||
"Document owner": "Besitzer des Dokuments",
|
||||
"Document title updated successfully": "Titel des Dokuments erfolgreich aktualisiert",
|
||||
"Download": "Herunterladen",
|
||||
"E-mail:": "E-Mail:",
|
||||
"Edition": "Bearbeiten",
|
||||
"Editor": "Editor",
|
||||
"Editor unavailable": "Editor nicht verfügbar",
|
||||
"Error during delete invitation": "Fehler beim Löschen der Einladung",
|
||||
"Error during invitation update": "Fehler beim Aktualisieren der Einladung",
|
||||
"Error during update invitation": "Fehler beim Aktualisieren der Einladung",
|
||||
"Error while deleting invitation": "Fehler beim Löschen der Einladung",
|
||||
"Export": "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",
|
||||
"Failed to create the invitation for {{email}}.": "Fehler beim Erstellen der Einladung für {{email}}.",
|
||||
"Find a member to add to the document": "Suchen Sie ein Mitglied, das dem Dokument hinzugefügt werden soll",
|
||||
"Format": "Format",
|
||||
"History": "Versionsverlauf",
|
||||
"If a member is editing, his works can be lost.": "Wenn ein Mitglied editiert, können seine Änderungen verloren gehen.",
|
||||
"Improvement and contact": "Verbesserungen und Kontakt",
|
||||
"Invitation sent to {{email}}.": "Einladung an {{email}} gesendet.",
|
||||
"Invite new members to {{title}}": "Neue Mitglieder zu {{title}} einladen",
|
||||
"Invited": "Eingeladen",
|
||||
"Invite": "Einladen",
|
||||
"It is the card information about the document.": "Es handelt sich um die Karteninformationen zum Dokument.",
|
||||
"It is the document title": "Es ist der Titel des Dokuments",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Es scheint, dass die von Ihnen gesuchte Seite nicht existiert oder nicht korrekt angezeigt werden kann.",
|
||||
"Language": "Sprache",
|
||||
"Last update: {{update}}": "Zuletzt aktualisiert: {{update}}",
|
||||
"Legal Notice": "Impressum",
|
||||
"Legal notice": "Impressum",
|
||||
"Link Copied !": "Link kopiert!",
|
||||
"Link parameters": "Link-Parameter",
|
||||
"List invitation card": "Einladungsliste anzeigen",
|
||||
"List members card": "Mitgliederliste anzeigen",
|
||||
"Load more": "Mehr anzeigen",
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Modal confirmation to restore the version": "Modale Bestätigung um die Version wiederherzustellen",
|
||||
"More docs": "Weitere Dokumente",
|
||||
"My docs": "Meine Dokumente",
|
||||
"Name": "Name",
|
||||
"New doc": "Neues Dokument",
|
||||
"No active search": "Keine aktive Suche",
|
||||
"No document found": "Kein Dokument gefunden",
|
||||
"No documents found": "Keine Dokumente gefunden",
|
||||
"No editor found": "Kein Editor gefunden",
|
||||
"No versions": "Keine Versionen",
|
||||
"OK": "OK",
|
||||
"Offline ?!": "Offline?!",
|
||||
"Only for people with access": "Nur für Personen mit Zugriff",
|
||||
"Only invited people can access": "Nur eingeladene Personen haben Zugriff",
|
||||
"Open the document options": "Öffnen Sie die Dokumentoptionen",
|
||||
"Open the header menu": "Öffne das Kopfzeilen-Menü",
|
||||
"Ouch !": "Autsch!",
|
||||
"Owner": "Besitzer",
|
||||
"PDF": "PDF",
|
||||
"Pending invitations": "Ausstehende Einladungen",
|
||||
"Personal data and cookies": "Personenbezogene Daten und Cookies",
|
||||
"Pin": "Anheften",
|
||||
"Pinned documents": "Angepinnte Dokumente",
|
||||
"Private": "Privat",
|
||||
"Public": "Öffentlich",
|
||||
"Read only, you cannot edit this document.": "Nur lesen: Sie können dieses Dokument nicht bearbeiten.",
|
||||
"Public document": "Öffentliches Dokument",
|
||||
"Quick search input": "Schnellsuche-Eingabe",
|
||||
"Reader": "Leser",
|
||||
"Reading": "Lesen",
|
||||
"Remove": "Löschen",
|
||||
"Rename": "Umbenennen",
|
||||
"Rephrase": "Umformulieren",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Role": "Rolle",
|
||||
"Search by email": "Nach E-Mail suchen",
|
||||
"Search": "Suchen",
|
||||
"Search modal": "Suche Modal",
|
||||
"Search user result": "Suchergebnis",
|
||||
"Select a document": "Dokument auswählen",
|
||||
"Select a version on the right to restore": "Wählen Sie rechts eine Version zum Wiederherstellen aus",
|
||||
"Share": "Teilen",
|
||||
"Share modal": "Teilen-Modal",
|
||||
"Share the document": "Dokument teilen",
|
||||
"Share with {{count}} users_many": "Teilen mit {{count}} Benutzern",
|
||||
"Share with {{count}} users_one": "Teilen mit {{count}} Benutzern",
|
||||
"Share with {{count}} users_other": "Teilen mit {{count}} Benutzern",
|
||||
"Shared with me": "Mit mir geteilt",
|
||||
"Something bad happens, please retry.": "Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
|
||||
"Table of contents": "Inhaltsverzeichnis",
|
||||
"Summarize": "Zusammenfassen",
|
||||
"Summary": "Zusammenfassung",
|
||||
"Template": "Vorlage",
|
||||
"The document has been deleted.": "Das Dokument wurde gelöscht.",
|
||||
"The invitation has been removed.": "Die Einladung wurde zurückgenommen.",
|
||||
"The member has been removed from the document": "Das Mitglied wurde aus dem Dokument entfernt",
|
||||
"The role has been updated": "Die Rolle wurde aktualisiert",
|
||||
"The role has been updated.": "Die Rolle wurde aktualisiert.",
|
||||
"The document visibility has been updated.": "Die Sichtbarkeit des Dokuments wurde aktualisiert.",
|
||||
"This accessibility statement applies to the site hosted on": "Diese Erklärung zur Barrierefreiheit gilt für die gehostete Seite",
|
||||
"This site does not display a cookie consent banner, why?": "",
|
||||
"Too many requests. Please wait 60 seconds.": "Zu viele Anfragen. Bitte warten Sie 60 Sekunden.",
|
||||
"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",
|
||||
"Unless otherwise stated, all content on this site is under": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
|
||||
"Unpin": "Lösen",
|
||||
"Untitled document": "Unbenanntes Dokument",
|
||||
"Updated at": "Aktualisiert am",
|
||||
"User {{email}} added to the document.": "Benutzer {{email}} wurde dem Dokument hinzugefügt.",
|
||||
"Validate": "Bestätigen",
|
||||
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Laden Sie Ihre Dokumente zu einem Microsoft Word, Open Office oder PDF Dokument hoch.",
|
||||
"Use as prompt": "Als Prompt verwenden",
|
||||
"Version history": "Versionsverlauf",
|
||||
"Version restored successfully": "Version erfolgreich wiederhergestellt",
|
||||
"We didn't find a mail matching, try to be more accurate": "Wir haben keine übereinstimmende E-Mail gefunden, versuchen Sie genauer zu sein",
|
||||
"Visibility": "Sichtbarkeit",
|
||||
"Visibility mode": "Sichtbarkeitseinstellungen",
|
||||
"Warning": "Warnung",
|
||||
"We try to respond within 2 working days.": "Wir versuchen, innerhalb von 2 Arbeitstagen zu antworten.",
|
||||
"Word / Open Office": "Word / Open Office",
|
||||
"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 cannot update the role or remove other owner.": "Sie können die Rolle nicht aktualisieren oder einen anderen Besitzer entfernen.",
|
||||
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
|
||||
@@ -96,21 +156,23 @@
|
||||
"AI seems busy! Please try again.": "L'IA semble occupée ! Veuillez réessayer.",
|
||||
"Accessibility": "Accessibilité",
|
||||
"Accessibility statement": "Déclaration d'accessibilité",
|
||||
"Add": "Ajouter",
|
||||
"Address:": "Adresse :",
|
||||
"Administrator": "Administrateur",
|
||||
"All docs": "Tous les documents",
|
||||
"Anonymous": "Anonyme",
|
||||
"Anyone on the internet with the link can view": "Les personnes disposant du lien peuvent y accéder",
|
||||
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
|
||||
"Anyone with the link can edit the document if they are logged in": "N'importe qui avec le lien peut éditer le document à condition qu'il soit connecté",
|
||||
"Anyone with the link can see the document": "N'importe qui avec le lien peut voir le document",
|
||||
"Anyone with the link can view the document if they are logged in": "N'importe qui avec le lien peut voir le document à condition qu'il soit connecté",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Êtes-vous sûr de vouloir supprimer le document \"{{title}}\" ?",
|
||||
"Authenticated": "Authentifié",
|
||||
"Back to home page": "Retour à l'accueil",
|
||||
"Can read and edit": "Peut lire et éditer",
|
||||
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
|
||||
"Cancel": "Annuler",
|
||||
"Choose a role": "Choisissez un rôle",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Compliance status": "État de conformité",
|
||||
"Confirm deletion": "Confirmer la suppression",
|
||||
"Connected": "Connecté",
|
||||
"Content modal to delete document": "Contenu modal pour supprimer le document",
|
||||
"Content modal to export the document": "Contenu modal pour exporter le document",
|
||||
"Convert Markdown": "Convertir le Markdown",
|
||||
@@ -121,35 +183,38 @@
|
||||
"Copyright": "Copyright",
|
||||
"Correct": "Corriger",
|
||||
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits - Réponse gratuite - 71120 75342 Paris CEDEX 07",
|
||||
"Delete": "Supprimer",
|
||||
"Delete a doc": "Supprimer un doc",
|
||||
"Delete document": "Supprimer le document",
|
||||
"Deleting the document \"{{title}}\"": "Suppression du document \"{{title}}\"",
|
||||
"Doc visibility card": "Carte de visibilité du doc",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Logo Docs",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.",
|
||||
"Document owner": "Propriétaire du document",
|
||||
"Document title updated successfully": "Titre du document mis à jour avec succès",
|
||||
"Download": "Télécharger",
|
||||
"E-mail:": "E-mail:",
|
||||
"Edition": "Édition",
|
||||
"Editor": "Éditeur",
|
||||
"Editor unavailable": "Éditeur indisponible",
|
||||
"Error during delete invitation": "Erreur lors de la suppression de l'invitation",
|
||||
"Error during invitation update": "Erreur lors de la mise à jour de l'invitation",
|
||||
"Error during update invitation": "Erreur lors de la mise à jour de l'invitation",
|
||||
"Error while deleting invitation": "Erreur lors de la suppression de l'invitation",
|
||||
"Established on December 20, 2023.": "Établi le 20 décembre 2023.",
|
||||
"Export": "Exporter",
|
||||
"Failed to add the member in the document.": "Impossible d'ajouter le membre dans le document.",
|
||||
"Failed to copy link": "Échec de la copie du lien",
|
||||
"Failed to copy to clipboard": "Échec de la copie dans le presse-papier",
|
||||
"Failed to create the invitation for {{email}}.": "Impossible de créer l'invitation pour {{email}}.",
|
||||
"Find a member to add to the document": "Trouver un membre à ajouter au document",
|
||||
"Format": "Format",
|
||||
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
|
||||
"History": "Historique",
|
||||
"How people can interact with the document": "Comment les gens peuvent interagir avec le document",
|
||||
"If a member is editing, his works can be lost.": "Si un membre est en train d'éditer, ses travaux peuvent être perdus.",
|
||||
"If you are unable to access a content or a service, you can contact the person responsible for https://lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.": "Si vous ne pouvez pas accéder à un contenu ou à un service, vous pouvez contacter la personne responsable de https://lasuite. umerique.gouv.fr pour être dirigé vers une alternative accessible ou pour obtenir le contenu sous une autre forme.",
|
||||
"Illustration:": "Illustration :",
|
||||
"Improvement and contact": "Amélioration et contact",
|
||||
"Invitation sent to {{email}}.": "Invitation envoyée à {{email}}.",
|
||||
"Invite new members to {{title}}": "Invitez de nouveaux membres à rejoindre {{title}}",
|
||||
"Invited": "Invité",
|
||||
"Invite": "Inviter",
|
||||
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
|
||||
"It is the document title": "Il s'agit du titre du document",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
||||
@@ -159,6 +224,7 @@
|
||||
"Legal Notice": "Mentions Legales",
|
||||
"Legal notice": "Mention légale",
|
||||
"Link Copied !": "Lien copié !",
|
||||
"Link parameters": "Paramètres du lien",
|
||||
"List invitation card": "Carte de liste d'invitation",
|
||||
"List members card": "Carte liste des membres",
|
||||
"Load more": "Afficher plus",
|
||||
@@ -170,50 +236,57 @@
|
||||
"My docs": "Mes documents",
|
||||
"Name": "Nom",
|
||||
"New doc": "Nouveau doc",
|
||||
"No active search": "Aucune recherche active",
|
||||
"No document found": "Aucun document trouvé",
|
||||
"No documents found": "Aucun document trouvé",
|
||||
"No editor found": "Pas d'éditeur trouvé",
|
||||
"No versions": "Aucune version",
|
||||
"Nothing exceptional, no special privileges related to a .gouv.fr.": "Rien d'exceptionnel, pas de privilèges spéciaux liés à un .gouv.fr.",
|
||||
"OK": "OK",
|
||||
"Offline ?!": "Hors-ligne ?!",
|
||||
"Only for authenticated users": "Uniquement pour les utilisateurs authentifiés",
|
||||
"Only for people with access": "Seulement pour les personnes avec accès",
|
||||
"Only invited people can access": "Seules les personnes invitées peuvent accéder",
|
||||
"Open the document options": "Ouvrir les options du document",
|
||||
"Open the header menu": "Ouvrir le menu d'en-tête",
|
||||
"Ouch !": "Aïe !",
|
||||
"Owner": "Propriétaire",
|
||||
"PDF": "PDF",
|
||||
"Pending invitations": "Invitations en attente",
|
||||
"Personal data and cookies": "Données personnelles et cookies",
|
||||
"Pin": "Épingler",
|
||||
"Pinned documents": "Documents épinglés",
|
||||
"Private": "Privé",
|
||||
"Public": "Public",
|
||||
"Public document": "Document public",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"Read only": "Lecture seule",
|
||||
"Read only, you cannot edit this document.": "En lecture seule, vous ne pouvez pas éditer ce document.",
|
||||
"Quick search input": "Saisie de recherche rapide",
|
||||
"Reader": "Lecteur",
|
||||
"Reading": "Lecture seul",
|
||||
"Remedies": "Voie de recours",
|
||||
"Remove": "Supprimer",
|
||||
"Rename": "Renommer",
|
||||
"Rephrase": "Reformuler",
|
||||
"Restore": "Restaurer",
|
||||
"Restricted": "Restreint",
|
||||
"Role": "Rôle",
|
||||
"Search by email": "Recherche par email",
|
||||
"Search": "Rechercher",
|
||||
"Search modal": "Modale de partage",
|
||||
"Search user result": "Résultat de la recherche utilisateur",
|
||||
"Select a document": "Sélectionnez un document",
|
||||
"Select a version on the right to restore": "Sélectionnez une version à droite à restaurer",
|
||||
"Send a letter by post (free of charge, no stamp needed):": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre):",
|
||||
"Share": "Partager",
|
||||
"Share modal": "Modale de partage",
|
||||
"Share the document": "Partager le document",
|
||||
"Share with {{count}} users_many": "Partager avec {{count}} utilisateurs",
|
||||
"Share with {{count}} users_one": "Partager avec {{count}} utilisateur",
|
||||
"Share with {{count}} users_other": "Partager avec {{count}} utilisateurs",
|
||||
"Shared with me": "Partagés avec moi",
|
||||
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
|
||||
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
|
||||
"Summarize": "Résumer",
|
||||
"Summary": "Sommaire",
|
||||
"Table of contents": "Table des matières",
|
||||
"Template": "Template",
|
||||
"The document has been deleted.": "Le document a bien été supprimé.",
|
||||
"The document visibility has been updated.": "La visibilité du document a été mise à jour.",
|
||||
"The invitation has been removed.": "L'invitation a été supprimée.",
|
||||
"The member has been removed from the document": "Le membre a été retiré du document",
|
||||
"The role has been updated": "Le rôle a été mis à jour",
|
||||
"The role has been updated.": "Le rôle a été mis à jour.",
|
||||
"The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse",
|
||||
"This accessibility statement applies to the site hosted on": "Cette déclaration d'accessibilité s'applique au site hébergé sur",
|
||||
"This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.",
|
||||
@@ -222,18 +295,19 @@
|
||||
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
|
||||
"This will protect your privacy, but will also prevent the owner from learning from your actions and creating a better experience for you and other users.": "Cela protégera votre vie privée, mais empêchera également le propriétaire d'apprendre de vos actions et de créer une meilleure expérience pour vous et les autres utilisateurs.",
|
||||
"Too many requests. Please wait 60 seconds.": "Trop de demandes. Veuillez patienter 60 secondes.",
|
||||
"Type a name or email": "Tapez un nom ou un email",
|
||||
"Type the name of a document": "Tapez le nom d'un document",
|
||||
"Unless otherwise stated, all content on this site is under": "Sauf mention contraire, tout le contenu de ce site est sous",
|
||||
"Unpin": "Désépingler",
|
||||
"Untitled document": "Document sans titre",
|
||||
"Updated at": "Mise à jour le",
|
||||
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Téléchargez vos documents dans un document Microsoft Word, Open Office ou PDF.",
|
||||
"Use as prompt": "Utiliser comme un prompt",
|
||||
"User {{email}} added to the document.": "L'utilisateur {{email}} a été ajouté au document.",
|
||||
"Validate": "Valider",
|
||||
"Version history": "Historique des versions",
|
||||
"Version restored successfully": "Version restaurée avec succès",
|
||||
"Visibility": "Visibilité",
|
||||
"Visibility mode": "Mode de visibilité",
|
||||
"Warning": "Attention",
|
||||
"We didn't find a mail matching, try to be more accurate": "Nous n'avons pas trouvé de correspondance par mail, essayez d'être plus précis",
|
||||
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure d’audience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
|
||||
"We try to respond within 2 working days.": "Nous essayons de répondre dans les 2 jours ouvrables.",
|
||||
"Word / Open Office": "Word / Open Office",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
@@ -35,8 +35,8 @@
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "6.0.2",
|
||||
"@types/ws": "8.5.13",
|
||||
"eslint-config-impress": "*",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-config-impress": "*",
|
||||
"jest": "29.7.0",
|
||||
"nodemon": "3.1.9",
|
||||
"supertest": "7.0.0",
|
||||
|
||||
@@ -93,4 +93,4 @@ releases:
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 1.10.0
|
||||
- version: 2.0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user