Compare commits

..

1 Commits

Author SHA1 Message Date
Manuel Raynaud
9427b17a15 save work 2026-04-27 16:21:15 +02:00
78 changed files with 1547 additions and 2543 deletions

View File

@@ -6,18 +6,10 @@ and this project adheres to
## [Unreleased]
### Changed
- 🐛(frontend) sanitize pasted and dropped content in document title #2210
- 🐛(backend) replace document creation table locks with retry strategy
## [v5.0.0] - 2026-04-08
### Added
- ✨(backend) create a dedicated endpoint to update document content #2171
- ⚡️(backend) stream s3 file content with a dedicated endpoint #2171
- ✨(backend) allow to use new ai feature using mistral sdk #2193
- ✨(backend) create a dedicated endpoint to update document content
- ⚡️(backend) stream s3 file content with a dedicated endpoint
### Changed
@@ -26,8 +18,6 @@ and this project adheres to
- ♿️(frontend) structure correctly 5xx error alerts #2128
- ♿️(frontend) make doc search result labels uniquely identifiable #2212
- ⬆️(backend) upgrade docspec to v3.0.x and adapt converter API #2220
- ✨(backend) make forward auth request uri header configurable #2241
- ♿️(frontend) fix sidebar resize handle for screen readers #2122
### Fixed
@@ -38,16 +28,11 @@ and this project adheres to
- 🐛(frontend) fix interlinking modal clipping #2213
- 🛂(frontend) fix cannot manage member on small screen #2226
- 🐛(backend) load jwks url when OIDC_RS_PRIVATE_KEY_STR is set
- 🐛(backend) Prevent moving document to its own descendant or self #2208
- 🐛(backend) return 400 when restoring a non-deleted document #2225
- 🐛(backend) fix race condition in reconciliation requests CSV import #2153
- 🐛(backend) create_for_owner: add accesses before saving doc content #2124
- 🐛(backend) enforce emoji validation for reactions #1965
### Removed
- 🔥(backend) remove deprecated descendants endpoint #2243
- 🔥(backend) remove content in document responses #2171
- 🔥(backend) remove content in document responses
## [v4.8.6] - 2026-04-08
@@ -86,6 +71,7 @@ and this project adheres to
- ⚡️(frontend) add jitter to WS reconnection #2162
- 🐛(frontend) fix tree pagination #2145
- 🐛(nginx) add page reconciliation on nginx #2154
- 🐛(backend) fix race condition in reconciliation requests CSV import #2153
## [v4.8.4] - 2026-03-25
@@ -107,6 +93,9 @@ and this project adheres to
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
### Fixed
- 🐛(backend) create_for_owner: add accesses before saving doc content #2124
## [v4.8.3] - 2026-03-23
@@ -1275,8 +1264,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v5.0.0...main
[v5.0.0]: https://github.com/suitenumerique/docs/releases/v5.0.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.8.6...main
[v4.8.6]: https://github.com/suitenumerique/docs/releases/v4.8.6
[v4.8.5]: https://github.com/suitenumerique/docs/releases/v4.8.5
[v4.8.4]: https://github.com/suitenumerique/docs/releases/v4.8.4

View File

@@ -16,8 +16,6 @@ the following command inside your docker container:
## [Unreleased]
### [5.0.0] - 2026-04-30
We made several changes around document content management leading to several breaking changes in the API.
- The endpoint `/api/v1.0/documents/{document_id}/content/` has been renamed in `/api/v1.0/documents/{document_id}/formatted-content/`
@@ -35,9 +33,6 @@ Other changes:
- The deprecated endpoint `/api/v1.0/documents/<document_id>/descendants` is removed. The search endpoint should be used instead.
- Upgrade docspec dependency to version >= 3.0.0
The docspec service has changed since version 3.0.0, we ware now compatible with this version and not with version 2.x.x anymore
- It is now possible to use the Mistral SDK instead of the OpenAI for the AI features. If your provider is compatible with the mistral API, we encourage you to use it.
- `AI_API_KEY` settings is renamed in `OPENAI_SDK_API_KEY` and is only used to congiure the OpenAi sdk
- `AI_BASE_URL` settings is renamed in `OPENAI_SDK_BASE_URL` and is only used to congiure the OpenAi sdk
## [4.6.0] - 2026-02-27

View File

@@ -9,16 +9,14 @@ These are the environment variables you can set for the `impress-backend` contai
| Option | Description | default |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| OPENAI_SDK_API_KEY | AI key to be used by the OpenAI python SDK | |
| OPENAI_SDK_BASE_URL | OpenAI compatible AI base url | |
| MISTRAL_SDK_API_KEY | AI key to be used by the Mistral python SDK /!\ Mistral sdk can be used only in async mode with uvicorn /!\ | |
| MISTRAL_SDK_BASE_URL | Mistral compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" } |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" }
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_FEATURE_BLOCKNOTE_ENABLED | Enable Blocknote AI options | false |
| AI_FEATURE_LEGACY_ENABLED | Enable legacyAI options | true |
| AI_FEATURE_BLOCKNOTE_ENABLED | Enable Blocknote AI options | false |
| AI_FEATURE_LEGACY_ENABLED | Enable legacyAI options | true |
| AI_MODEL | AI Model to use | |
| AI_VERCEL_SDK_VERSION | The vercel AI SDK version used | 6 |
| AI_VERCEL_SDK_VERSION | The vercel AI SDK version used | 6 |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
@@ -93,7 +91,6 @@ These are the environment variables you can set for the `impress-backend` contai
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
| MEDIA_BASE_URL | | |
| MEDIA_AUTH_ORIGINAL_URL_HEADER | Parameter containing the original request URL, as seen at the media auth endpoint, in CGI/WSGI form (HTTP_HEADER_NAME_ALL_CAPS_WITH_UNDERSCORES) | HTTP_X_ORIGINAL_URL |
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
@@ -134,7 +131,6 @@ These are the environment variables you can set for the `impress-backend` contai
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
| TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS | Number of attempts to create a document before failing. | 10 |
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
| USER_ONBOARDING_DOCUMENTS | A list of documents IDs for which a read-only access will be created for new s | [] |
| USER_ONBOARDING_SANDBOX_DOCUMENT | ID of a template sandbox document that will be duplicated for new users | |

View File

@@ -71,6 +71,14 @@ OIDC_RS_ALLOWED_AUDIENCES=""
# User reconciliation
USER_RECONCILIATION_FORM_URL=http://localhost:3000
# AI
AI_FEATURE_ENABLED=true
AI_FEATURE_BLOCKNOTE_ENABLED=true
AI_FEATURE_LEGACY_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama
# Collaboration
COLLABORATION_API_URL=http://y-provider-development:4444/collaboration/api/
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000

View File

@@ -7,23 +7,22 @@ from base64 import b64decode
from os.path import splitext
from django.conf import settings
from django.db import connection, transaction
from django.db.models import Q
from django.utils.functional import lazy
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
import emoji
import magic
from rest_framework import serializers
from core import choices, enums, models, validators
from core.services import mime_types
from core.services.ai_services.legacy import AI_ACTIONS
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
Converter,
)
from core.utils.treebeard import create_tree_node_with_retry
class UserSerializer(serializers.ModelSerializer):
@@ -467,12 +466,18 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
{"content": ["Could not convert content"]}
) from err
document = create_tree_node_with_retry(
lambda: models.Document.add_root(
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
document = models.Document.add_root(
title=validated_data["title"],
creator=user,
)
)
if user:
# Associate the document with the pre-existing user
@@ -870,12 +875,6 @@ class ReactionSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "created_at", "users"]
def validate_emoji(self, value):
"""Ensure the reaction is a single emoji."""
if not emoji.is_emoji(value):
raise serializers.ValidationError("Reaction must be a single valid emoji.")
return value
class CommentSerializer(serializers.ModelSerializer):
"""Serialize comments (nested under a thread) with reactions and abilities."""

View File

@@ -44,13 +44,11 @@ from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from treebeard.exceptions import InvalidMoveToDescendant
from core import authentication, choices, enums, models
from core.api.filters import remove_accents
from core.services import mime_types
from core.services.ai_services.blocknote import AIService
from core.services.ai_services.legacy import get_legacy_ai_service
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.converter_services import (
ConversionError,
@@ -67,10 +65,11 @@ from core.services.search_indexers import (
get_visited_document_ids_of,
)
from core.tasks.mail import send_ask_for_access_mail
from core.utils.paths import filter_descendants
from core.utils.treebeard import create_tree_node_with_retry
from core.utils.users import users_sharing_documents_with
from core.utils.yjs import extract_attachments
from core.utils import (
extract_attachments,
filter_descendants,
users_sharing_documents_with,
)
from ..enums import FeatureFlag, SearchType
from . import permissions, serializers, utils
@@ -707,12 +706,18 @@ class DocumentViewSet(
{"file": ["Could not convert file content"]}
) from err
obj = create_tree_node_with_retry(
lambda: models.Document.add_root(
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
obj = models.Document.add_root(
creator=self.request.user,
**serializer.validated_data,
)
)
serializer.instance = obj
models.DocumentAccess.objects.create(
document=obj,
@@ -956,13 +961,7 @@ class DocumentViewSet(
status=status.HTTP_400_BAD_REQUEST,
)
try:
document.move(target_document, pos=position)
except InvalidMoveToDescendant:
return drf.response.Response(
{"target_document_id": "Cannot move a document to its own descendant."},
status=status.HTTP_400_BAD_REQUEST,
)
document.move(target_document, pos=position)
# Make sure we have at least one owner
if (
@@ -990,10 +989,7 @@ class DocumentViewSet(
Restore a soft-deleted document if it was deleted less than x days ago.
"""
document = self.get_object()
try:
document.restore()
except RuntimeError as err:
raise drf.exceptions.ValidationError({"detail": str(err)}) from err
document.restore()
return drf_response.Response(
{"detail": "Document has been successfully restored."},
@@ -1016,12 +1012,16 @@ class DocumentViewSet(
)
serializer.is_valid(raise_exception=True)
child_document = create_tree_node_with_retry(
lambda: document.add_child(
with transaction.atomic():
# "select_for_update" locks the table to ensure safe concurrent access
locked_parent = models.Document.objects.select_for_update().get(
pk=document.pk
)
child_document = locked_parent.add_child(
creator=request.user,
**serializer.validated_data,
)
)
# Set the created instance to the serializer
serializer.instance = child_document
@@ -1752,13 +1752,10 @@ class DocumentViewSet(
def _auth_get_original_url(self, request):
"""
Extracts and parses the original URL from the configured parameter header.
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
Raises PermissionDenied if the header is missing.
The original url is passed by reverse proxy in the header specified by the
MEDIA_AUTH_ORIGINAL_URL_HEADER setting.
For nginx (the default) this is set to HTTP_X_ORIGINAL_URL.
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
See corresponding ingress configuration in Helm chart and read about the
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
is configured to do this.
@@ -1769,14 +1766,9 @@ class DocumentViewSet(
reasons.
"""
# Extract the original URL from the request header
original_url = request.META.get(settings.MEDIA_AUTH_ORIGINAL_URL_HEADER)
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
if not original_url:
logger.debug(
"Missing %s header in subrequest. "
"Maybe you need to set MEDIA_AUTH_ORIGINAL_URL_HEADER correctly for your ingress"
" proxy.",
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER,
)
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
raise drf.exceptions.PermissionDenied()
logger.debug("Original url: '%s'", original_url)
@@ -2123,16 +2115,13 @@ class DocumentViewSet(
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED or not settings.AI_FEATURE_LEGACY_ENABLED:
raise ValidationError("AI feature is not enabled.")
serializer = serializers.AITransformSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data["text"]
action = serializer.validated_data["action"]
response = get_legacy_ai_service().transform(text, action)
response = AIService().transform(text, action)
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
@@ -2154,16 +2143,13 @@ class DocumentViewSet(
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED or not settings.AI_FEATURE_LEGACY_ENABLED:
raise ValidationError("AI feature is not enabled.")
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data["text"]
language = serializer.validated_data["language"]
response = get_legacy_ai_service().translate(text, language)
response = AIService().translate(text, language)
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
@@ -2274,7 +2260,7 @@ class DocumentViewSet(
GET /api/v1.0/documents/<resource_id>/cors-proxy
Act like a proxy to fetch external resources and bypass CORS restrictions.
"""
url = request.query_params.get("url", "").strip()
url = request.query_params.get("url")
if not url:
return drf.response.Response(
{"detail": "Missing 'url' query parameter"},

View File

@@ -231,10 +231,9 @@ class ReactionFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.Reaction
skip_postgeneration_save = True
comment = factory.SubFactory(CommentFactory)
emoji = factory.Faker("emoji")
emoji = "test"
@factory.post_generation
def users(self, create, extracted, **kwargs):

View File

@@ -9,7 +9,7 @@ from django.db import migrations, models
from botocore.exceptions import ClientError
import core.models
from core.utils.yjs import extract_attachments
from core.utils import extract_attachments
def populate_attachments_on_all_documents(apps, schema_editor):

View File

@@ -19,7 +19,7 @@ from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.mail import send_mail
from django.db import models, transaction
from django.db import connection, models, transaction
from django.db.models.functions import Left, Length
from django.template.loader import render_to_string
from django.utils import timezone
@@ -39,7 +39,6 @@ from core.choices import (
RoleChoices,
get_equivalent_link_definition,
)
from core.utils.treebeard import create_tree_node_with_retry
from core.validators import sub_validator
logger = getLogger(__name__)
@@ -266,6 +265,8 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
duplicate the sandbox document for the user
"""
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
# transaction.atomic is used in a context manager to avoid a transaction if
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
@@ -275,15 +276,20 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
sandbox_id,
)
return
with transaction.atomic():
sandbox_document = create_tree_node_with_retry(
lambda: Document.add_root(
title=template_document.title,
content=template_document.content,
attachments=template_document.attachments,
duplicated_from=template_document,
creator=self,
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
sandbox_document = Document.add_root(
title=template_document.title,
content=template_document.content,
attachments=template_document.attachments,
duplicated_from=template_document,
creator=self,
)
DocumentAccess.objects.create(

View File

@@ -7,17 +7,15 @@ import os
import queue
import threading
from collections.abc import AsyncIterator, Iterator
from functools import cache
from typing import Any, Dict, Union
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from langfuse import get_client
from langfuse.openai import OpenAI as OpenAI_Langfuse
from pydantic_ai import Agent, DeferredToolRequests
from pydantic_ai.models.mistral import MistralModel
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.mistral import MistralProvider
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.tools import ToolDefinition
from pydantic_ai.toolsets.external import ExternalToolset
@@ -26,6 +24,13 @@ from pydantic_ai.ui.vercel_ai import VercelAIAdapter
from pydantic_ai.ui.vercel_ai.request_types import RequestData, TextUIPart, UIMessage
from rest_framework.request import Request
from core import enums
if settings.LANGFUSE_PUBLIC_KEY:
OpenAI = OpenAI_Langfuse
else:
from openai import OpenAI
log = logging.getLogger(__name__)
BLOCKNOTE_TOOL_STRICT_PROMPT = """
@@ -59,6 +64,50 @@ IDs ALWAYS end with "$". Use ids EXACTLY as provided.
Return ONLY the JSON tool input. No prose, no markdown.
"""
AI_ACTIONS = {
"prompt": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"correct": (
"Correct grammar and spelling of the markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"rephrase": (
"Rephrase the given markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"summarize": (
"Summarize the markdown text, preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"beautify": (
"Add formatting to the text to make it more readable. "
"Do not provide any other information. "
"Preserve the language."
),
"emojify": (
"Add emojis to the important parts of the text. "
"Do not provide any other information. "
"Preserve the language."
),
}
AI_TRANSLATE = (
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
)
def convert_async_generator_to_sync(async_gen: AsyncIterator[str]) -> Iterator[str]:
"""Convert an async generator to a sync generator."""
@@ -94,41 +143,47 @@ def convert_async_generator_to_sync(async_gen: AsyncIterator[str]) -> Iterator[s
thread.join()
@cache
def configure_pydantic_model_provider() -> OpenAIChatModel | MistralModel:
"""Configure a pydantic Model and return it."""
if (
settings.OPENAI_SDK_API_KEY
and settings.OPENAI_SDK_BASE_URL
and settings.AI_MODEL
):
return OpenAIChatModel(
settings.AI_MODEL,
provider=OpenAIProvider(
api_key=settings.OPENAI_SDK_API_KEY,
base_url=settings.OPENAI_SDK_BASE_URL,
),
)
if (
settings.MISTRAL_SDK_API_KEY
and settings.MISTRAL_SDK_BASE_URL
and settings.AI_MODEL
):
return MistralModel(
settings.AI_MODEL,
provider=MistralProvider(
api_key=settings.MISTRAL_SDK_API_KEY,
base_url=settings.MISTRAL_SDK_BASE_URL,
),
)
raise ImproperlyConfigured("AI configuration not set")
class AIService:
"""Service class for AI-related operations."""
def __init__(self):
"""Ensure that the AI configuration is set properly."""
if (
settings.AI_BASE_URL is None
or settings.AI_API_KEY is None
or settings.AI_MODEL is None
):
raise ImproperlyConfigured("AI configuration not set")
self.client = OpenAI(base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY)
def call_ai_api(self, system_content, text):
"""Helper method to call the OpenAI API and process the response."""
response = self.client.chat.completions.create(
model=settings.AI_MODEL,
messages=[
{"role": "system", "content": system_content},
{"role": "user", "content": text},
],
)
content = response.choices[0].message.content
if not content:
raise RuntimeError("AI response does not contain an answer")
return {"answer": content}
def transform(self, text, action):
"""Transform text based on specified action."""
system_content = AI_ACTIONS[action]
return self.call_ai_api(system_content, text)
def translate(self, text, language):
"""Translate text to a specified language."""
language_display = enums.ALL_LANGUAGES.get(language, language)
system_content = AI_TRANSLATE.format(language=language_display)
return self.call_ai_api(system_content, text)
@staticmethod
def inject_document_state_messages(
messages: list[UIMessage],
@@ -269,9 +324,13 @@ class AIService:
langfuse.auth_check()
Agent.instrument_all()
agent = Agent(
configure_pydantic_model_provider(), instrument=instrument_enabled
model = OpenAIChatModel(
settings.AI_MODEL,
provider=OpenAIProvider(
base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY
),
)
agent = Agent(model, instrument=instrument_enabled)
accept = request.META.get("HTTP_ACCEPT", SSE_CONTENT_TYPE)

View File

@@ -1,201 +0,0 @@
"""Module dedicated to the legacy ai services."""
import logging
from abc import ABC, abstractmethod
from functools import cache
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from langfuse import get_client, observe
from langfuse.openai import OpenAI as OpenAI_Langfuse
from mistralai import Mistral
from core import enums
if settings.LANGFUSE_PUBLIC_KEY:
OpenAI = OpenAI_Langfuse
else:
from openai import OpenAI
log = logging.getLogger(__name__)
AI_ACTIONS = {
"prompt": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"correct": (
"Correct grammar and spelling of the markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"rephrase": (
"Rephrase the given markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"summarize": (
"Summarize the markdown text, preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"beautify": (
"Add formatting to the text to make it more readable. "
"Do not provide any other information. "
"Preserve the language."
),
"emojify": (
"Add emojis to the important parts of the text. "
"Do not provide any other information. "
"Preserve the language."
),
}
AI_TRANSLATE = (
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown delimiters."
)
class LegacyAiClient(ABC):
"""abstract class for legacy client."""
@abstractmethod
def call_ai_api(self, system_content, text) -> str:
"""Abstract method call_ai_api."""
class LegacyAiServiceMistralClient(LegacyAiClient):
"""ai_service using mistral sdk for the legacy ai feature."""
def __init__(self):
"""Configure mistral sdk"""
if (
not settings.MISTRAL_SDK_API_KEY
or not settings.MISTRAL_SDK_BASE_URL
or not settings.AI_MODEL
):
raise ImproperlyConfigured("Mistral sdk configuration not set")
self.client = Mistral(
api_key=settings.MISTRAL_SDK_API_KEY,
server_url=settings.MISTRAL_SDK_BASE_URL,
)
@observe(as_type="generation")
def call_ai_api(self, system_content, text) -> str:
langfuse = None
messages = [
{"role": "system", "content": system_content},
{"role": "user", "content": text},
]
if settings.LANGFUSE_PUBLIC_KEY:
langfuse = get_client()
langfuse.auth_check()
langfuse.update_current_generation(
input=messages,
model=settings.AI_MODEL,
)
response = self.client.chat.complete(
model=settings.AI_MODEL,
messages=messages,
stream=False,
)
if langfuse:
langfuse.update_current_generation(
usage_details={
"input": response.usage.prompt_tokens,
"output": response.usage.completion_tokens,
},
output=response.choices[0].message.content,
)
return response.choices[0].message.content
class LegacyAiServiceOpenAiClient(LegacyAiClient):
"""ai_service using OpenAI client for the legacy ai feature."""
def __init__(self):
"""configure OpenAI client."""
if (
not settings.OPENAI_SDK_BASE_URL
or not settings.OPENAI_SDK_API_KEY
or not settings.AI_MODEL
):
raise ImproperlyConfigured("OpenAI configuration not set")
self.client = OpenAI(
base_url=settings.OPENAI_SDK_BASE_URL, api_key=settings.OPENAI_SDK_API_KEY
)
def call_ai_api(self, system_content, text) -> str:
response = self.client.chat.completions.create(
model=settings.AI_MODEL,
messages=[
{"role": "system", "content": system_content},
{"role": "user", "content": text},
],
)
return response.choices[0].message.content
class LegacyAIService:
"""Legacy ai service used by transform and translate actions."""
def __init__(self, ai_client: LegacyAiClient):
"""Assign client to the service."""
self.ai_client = ai_client
def call_ai_api(self, system_content, text):
"""Helper method to call the OpenAI API and process the response."""
content = self.ai_client.call_ai_api(system_content, text)
if not content:
raise RuntimeError("AI response does not contain an answer")
return {"answer": content}
def transform(self, text, action):
"""Transform text based on specified action."""
system_content = AI_ACTIONS[action]
return self.call_ai_api(system_content, text)
def translate(self, text, language):
"""Translate text to a specified language."""
language_display = enums.ALL_LANGUAGES.get(language, language)
system_content = AI_TRANSLATE.format(language=language_display)
return self.call_ai_api(system_content, text)
@cache
def get_legacy_ai_service() -> LegacyAIService:
"""Helper responsible to correctly instantiate and configure legacy ai service."""
ai_client = None
if settings.MISTRAL_SDK_API_KEY:
ai_client = LegacyAiServiceMistralClient()
if settings.OPENAI_SDK_API_KEY:
ai_client = LegacyAiServiceOpenAiClient()
if not ai_client:
raise ImproperlyConfigured(
"trying to configure legacy ai_service but missing client configuration."
)
return LegacyAIService(ai_client)

View File

@@ -12,11 +12,8 @@ from django.utils.module_loading import import_string
import requests
from core import models
from core import models, utils
from core.enums import SearchType
from core.utils.dicts import get_value_by_pattern
from core.utils.paths import get_ancestor_to_descendants_map
from core.utils.yjs import base64_yjs_to_text
logger = logging.getLogger(__name__)
@@ -47,7 +44,7 @@ def get_batch_accesses_by_users_and_teams(paths):
Get accesses related to a list of document paths,
grouped by users and teams, including all ancestor paths.
"""
ancestor_map = get_ancestor_to_descendants_map(
ancestor_map = utils.get_ancestor_to_descendants_map(
paths, steplen=models.Document.steplen
)
ancestor_paths = list(ancestor_map.keys())
@@ -300,7 +297,7 @@ class FindDocumentIndexer(BaseDocumentIndexer):
>>> get_title({"id": 1})
""
"""
titles = get_value_by_pattern(source, r"^title\.")
titles = utils.get_value_by_pattern(source, r"^title\.")
for title in titles:
if title:
return title
@@ -321,7 +318,7 @@ class FindDocumentIndexer(BaseDocumentIndexer):
"""
doc_path = document.path
doc_content = document.content
text_content = base64_yjs_to_text(doc_content) if doc_content else ""
text_content = utils.base64_yjs_to_text(doc_content) if doc_content else ""
return {
"id": str(document.id),

View File

@@ -11,7 +11,7 @@ from django.dispatch import receiver
from core import models
from core.tasks.search import trigger_batch_document_indexer
from core.utils.users import get_users_sharing_documents_with_cache_key
from core.utils import get_users_sharing_documents_with_cache_key
@receiver(signals.post_save, sender=models.Document)

View File

@@ -11,7 +11,6 @@ import pytest
from rest_framework.test import APIClient
from core import factories
from core.services.ai_services.blocknote import configure_pydantic_model_provider
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@@ -21,14 +20,13 @@ pytestmark = pytest.mark.django_db
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://localhost-ai:12345/"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_BASE_URL = "http://localhost-ai:12345/"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
configure_pydantic_model_provider.cache_clear()
@override_settings(
@@ -67,7 +65,7 @@ def test_api_documents_ai_proxy_anonymous_forbidden(reach, role):
@override_settings(AI_ALLOW_REACH_FROM="public")
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_anonymous_success(mock_stream):
"""
Anonymous users should be able to request AI proxy to a document
@@ -151,7 +149,7 @@ def test_api_documents_ai_proxy_authenticated_forbidden(reach, role):
("public", "editor"),
],
)
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_authenticated_success(mock_stream, reach, role):
"""
Authenticated users should be able to request AI proxy to a document
@@ -207,7 +205,7 @@ def test_api_documents_ai_proxy_reader(via, mock_user_teams):
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_success(mock_stream, via, role, mock_user_teams):
"""Users with sufficient permissions should be able to request AI proxy."""
user = factories.UserFactory()
@@ -268,7 +266,7 @@ def test_api_documents_ai_proxy_ai_feature_disabled(settings, setting_to_disable
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_throttling_document(mock_stream):
"""
Throttling per document should be triggered on the AI proxy endpoint.
@@ -306,7 +304,7 @@ def test_api_documents_ai_proxy_throttling_document(mock_stream):
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_throttling_user(mock_stream):
"""
Throttling per user should be triggered on the AI proxy endpoint.
@@ -341,7 +339,7 @@ def test_api_documents_ai_proxy_throttling_user(mock_stream):
}
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_returns_streaming_response(mock_stream):
"""AI proxy should return a StreamingHttpResponse with correct headers."""
user = factories.UserFactory()

View File

@@ -2,62 +2,47 @@
Test AI transform API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.services.ai_services.legacy import get_legacy_ai_service
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture
def ai_settings(settings):
def ai_settings():
"""Fixture to set AI settings."""
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_MODEL = "llama"
@pytest.fixture(autouse=True)
def clear_openai_client_config():
"""Clear the _configure_legacy_openai_client cache"""
get_legacy_ai_service.cache_clear()
with override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="llama"
):
yield
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role, ai_allow_reach_from",
"reach, role",
[
("restricted", "reader", "public"),
("restricted", "reader", "authenticated"),
("restricted", "reader", "restricted"),
("restricted", "editor", "public"),
("restricted", "editor", "authenticated"),
("restricted", "editor", "restricted"),
("authenticated", "reader", "public"),
("authenticated", "reader", "authenticated"),
("authenticated", "reader", "restricted"),
("authenticated", "editor", "public"),
("authenticated", "editor", "authenticated"),
("authenticated", "editor", "restricted"),
("public", "reader", "public"),
("public", "reader", "authenticated"),
("public", "reader", "restricted"),
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_transform_anonymous_forbidden(
reach, role, ai_allow_reach_from, settings
):
def test_api_documents_ai_transform_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI transform if the link reach
and role don't allow it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
@@ -69,14 +54,14 @@ def test_api_documents_ai_transform_anonymous_forbidden(
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_success(mock_create, settings):
def test_api_documents_ai_transform_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = "public"
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
@@ -103,17 +88,14 @@ def test_api_documents_ai_transform_anonymous_success(mock_create, settings):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@pytest.mark.parametrize("ai_allow_reach_from", ["authenticated", "restricted"])
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_limited_by_setting(
mock_create, ai_allow_reach_from, settings
):
def test_api_documents_ai_transform_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
@@ -194,8 +176,8 @@ def test_api_documents_ai_transform_authenticated_success(mock_create, reach, ro
"role": "system",
"content": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters. Preserve the language and markdown formatting. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
@@ -271,8 +253,8 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te
"role": "system",
"content": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters. Preserve the language and markdown formatting. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
@@ -282,7 +264,6 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te
)
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_transform_empty_text():
"""The text should not be empty when requesting AI transform."""
user = factories.UserFactory()
@@ -299,7 +280,6 @@ def test_api_documents_ai_transform_empty_text():
assert response.json() == {"text": ["This field may not be blank."]}
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_transform_invalid_action():
"""The action should valid when requesting AI transform."""
user = factories.UserFactory()
@@ -316,14 +296,14 @@ def test_api_documents_ai_transform_invalid_action():
assert response.json() == {"action": ['"invalid" is not a valid choice.']}
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_throttling_document(mock_create, settings):
def test_api_documents_ai_transform_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
settings.AI_DOCUMENT_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
@@ -349,14 +329,14 @@ def test_api_documents_ai_transform_throttling_document(mock_create, settings):
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_throttling_user(mock_create, settings):
def test_api_documents_ai_transform_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
settings.AI_USER_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
user = factories.UserFactory()
client = APIClient()
client.force_login(user)

View File

@@ -2,32 +2,27 @@
Test AI translate API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.services.ai_services.legacy import get_legacy_ai_service
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture
def ai_settings(settings):
def ai_settings():
"""Fixture to set AI settings."""
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_MODEL = "llama"
@pytest.fixture(autouse=True)
def clear_openai_client_config():
"clear the configure_legacy_openai_client cache"
get_legacy_ai_service.cache_clear()
with override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="llama"
):
yield
def test_api_documents_ai_translate_viewset_options_metadata():
@@ -50,34 +45,24 @@ def test_api_documents_ai_translate_viewset_options_metadata():
}
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role, ai_allow_reach_from",
"reach, role",
[
("restricted", "reader", "public"),
("restricted", "reader", "authenticated"),
("restricted", "reader", "restricted"),
("restricted", "editor", "public"),
("restricted", "editor", "authenticated"),
("restricted", "editor", "restricted"),
("authenticated", "reader", "public"),
("authenticated", "reader", "authenticated"),
("authenticated", "reader", "restricted"),
("authenticated", "editor", "public"),
("authenticated", "editor", "authenticated"),
("authenticated", "editor", "restricted"),
("public", "reader", "public"),
("public", "reader", "authenticated"),
("public", "reader", "restricted"),
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_translate_anonymous_forbidden(
reach, role, ai_allow_reach_from, settings
):
def test_api_documents_ai_translate_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI translate if the link reach
and role don't allow it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
@@ -89,14 +74,14 @@ def test_api_documents_ai_translate_anonymous_forbidden(
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_success(mock_create, settings):
def test_api_documents_ai_translate_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = "public"
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
@@ -117,9 +102,7 @@ def test_api_documents_ai_translate_anonymous_success(mock_create, settings):
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -127,17 +110,14 @@ def test_api_documents_ai_translate_anonymous_success(mock_create, settings):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@pytest.mark.parametrize("ai_allow_reach_from", ["authenticated", "restricted"])
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_limited_by_setting(
mock_create, ai_allow_reach_from, settings
):
def test_api_documents_ai_translate_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
@@ -221,9 +201,7 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -300,9 +278,7 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -310,7 +286,6 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
)
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_translate_empty_text():
"""The text should not be empty when requesting AI translate."""
user = factories.UserFactory()
@@ -327,7 +302,6 @@ def test_api_documents_ai_translate_empty_text():
assert response.json() == {"text": ["This field may not be blank."]}
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_translate_invalid_action():
"""The action should valid when requesting AI translate."""
user = factories.UserFactory()
@@ -344,14 +318,14 @@ def test_api_documents_ai_translate_invalid_action():
assert response.json() == {"language": ['"invalid" is not a valid choice.']}
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_throttling_document(mock_create, settings):
def test_api_documents_ai_translate_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI translate endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
settings.AI_DOCUMENT_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
@@ -377,14 +351,14 @@ def test_api_documents_ai_translate_throttling_document(mock_create, settings):
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_throttling_user(mock_create, settings):
def test_api_documents_ai_translate_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI translate endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
settings.AI_USER_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
user = factories.UserFactory()
client = APIClient()
client.force_login(user)

View File

@@ -644,13 +644,11 @@ def test_create_reaction_anonymous_user_public_document(link_role):
document = factories.DocumentFactory(link_reach="public", link_role=link_role)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 401
@@ -666,14 +664,12 @@ def test_create_reaction_authenticated_user_public_document():
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 403
@@ -688,19 +684,17 @@ def test_create_reaction_authenticated_user_accessible_public_document():
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 201
assert models.Reaction.objects.filter(
comment=comment, emoji=reaction.emoji, users__in=[user]
comment=comment, emoji="test", users__in=[user]
).exists()
@@ -715,14 +709,12 @@ def test_create_reaction_authenticated_user_connected_document_link_role_reader(
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 403
@@ -745,19 +737,17 @@ def test_create_reaction_authenticated_user_connected_document(link_role):
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 201
assert models.Reaction.objects.filter(
comment=comment, emoji=reaction.emoji, users__in=[user]
comment=comment, emoji="test", users__in=[user]
).exists()
@@ -770,14 +760,12 @@ def test_create_reaction_authenticated_user_restricted_accessible_document():
document = factories.DocumentFactory(link_reach="restricted")
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 403
@@ -793,14 +781,12 @@ def test_create_reaction_authenticated_user_restricted_accessible_document_role_
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 403
@@ -820,70 +806,26 @@ def test_create_reaction_authenticated_user_restricted_accessible_document_role_
document = factories.DocumentFactory(link_reach="restricted", users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
reaction = factories.ReactionFactory(comment=comment)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
{"emoji": "test"},
)
assert response.status_code == 201
assert models.Reaction.objects.filter(
comment=comment, emoji=reaction.emoji, users__in=[user]
comment=comment, emoji="test", users__in=[user]
).exists()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": reaction.emoji},
)
assert response.status_code == 400
assert response.json() == {"user_already_reacted": True}
def test_create_reaction_invalid_emoji():
"""Users should not be able to submit non-emojis as reactions."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.RoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "test"},
)
assert response.status_code == 400
assert "Reaction must be a single valid emoji." in str(response.json())
def test_create_reaction_multiple_emojis():
"""Users should not be able to submit multiple emojis as a single reaction."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.RoleChoices.COMMENTER)]
)
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
f"comments/{comment.id!s}/reactions/",
{"emoji": "🐛🐛"},
)
assert response.status_code == 400
assert "Reaction must be a single valid emoji." in str(response.json())
assert response.json() == {"user_already_reacted": True}
# Delete reaction

View File

@@ -55,31 +55,6 @@ def test_api_docs_cors_proxy_valid_url(mock_getaddrinfo):
assert response.streaming_content
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
@responses.activate
def test_api_docs_cors_proxy_url_with_surrounding_whitespace(mock_getaddrinfo):
"""
URLs with leading or trailing whitespace must still be proxied successfully,
otherwise images whose `src` has stray whitespace are missing from the PDF export.
"""
document = factories.DocumentFactory(link_reach="public")
# Mock DNS resolution to return a public IP address
mock_getaddrinfo.return_value = [
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
]
client = APIClient()
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url= {url_to_fetch} "
)
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.streaming_content
def test_api_docs_cors_proxy_without_url_query_string():
"""Test the CORS proxy API for documents without a URL query string."""
document = factories.DocumentFactory(link_reach="public")

View File

@@ -6,6 +6,7 @@ from io import BytesIO
from urllib.parse import urlparse
from uuid import uuid4
from django.conf import settings
from django.core.files.storage import default_storage
from django.utils import timezone
@@ -36,7 +37,7 @@ def test_api_documents_media_auth_unkown_document():
assert models.Document.objects.exists() is False
def test_api_documents_media_auth_anonymous_public(settings):
def test_api_documents_media_auth_anonymous_public():
"""Anonymous users should be able to retrieve attachments linked to a public document"""
document_id = uuid4()
filename = f"{uuid4()!s}.jpg"
@@ -138,7 +139,7 @@ def test_api_documents_media_auth_anonymous_authenticated_or_restricted(reach):
assert "Authorization" not in response
def test_api_documents_media_auth_anonymous_attachments(settings):
def test_api_documents_media_auth_anonymous_attachments():
"""
Declaring a media key as original attachment on a document to which
a user has access should give them access to the attachment file
@@ -201,9 +202,7 @@ def test_api_documents_media_auth_anonymous_attachments(settings):
@pytest.mark.parametrize("reach", ["public", "authenticated"])
def test_api_documents_media_auth_authenticated_public_or_authenticated(
reach, settings
):
def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
"""
Authenticated users who are not related to a document should be able to retrieve
attachments related to a document with public or authenticated link reach.
@@ -285,7 +284,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, settings):
def test_api_documents_media_auth_related(via, mock_user_teams):
"""
Users who have a specific access to a document, whatever the role, should be able to
retrieve related attachments.
@@ -369,7 +368,7 @@ def test_api_documents_media_auth_not_ready_status():
assert response.status_code == 403
def test_api_documents_media_auth_missing_status_metadata(settings):
def test_api_documents_media_auth_missing_status_metadata():
"""Attachments without status metadata should be considered as ready"""
document_id = uuid4()
filename = f"{uuid4()!s}.jpg"
@@ -413,51 +412,3 @@ def test_api_documents_media_auth_missing_status_metadata(settings):
timeout=1,
)
assert response.content.decode("utf-8") == "my prose"
def test_api_documents_media_auth_anonymous_public_custom_origin_header(settings):
"""Changing the setting MEDIA_AUTH_ORIGINAL_URL_HEADER to match other header should work"""
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER = "HTTP_X_FORWARDED_URI"
document_id = uuid4()
filename = f"{uuid4()!s}.jpg"
key = f"{document_id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
original_url = f"http://localhost/media/{key:s}"
now = timezone.now()
with freeze_time(now):
response = APIClient().get(
"/api/v1.0/documents/media-auth/", HTTP_X_FORWARDED_URI=original_url
)
assert response.status_code == 200
authorization = response["Authorization"]
assert "AWS4-HMAC-SHA256 Credential=" in authorization
assert (
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
in authorization
)
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
response = requests.get(
file_url,
headers={
"authorization": authorization,
"x-amz-date": response["x-amz-date"],
"x-amz-content-sha256": response["x-amz-content-sha256"],
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
},
timeout=1,
)
assert response.content.decode("utf-8") == "my prose"

View File

@@ -438,92 +438,3 @@ def test_api_documents_move_authenticated_deleted_target_as_sibling(position):
# Verify that the document has not moved
document.refresh_from_db()
assert document.is_root() is True
@pytest.mark.parametrize("position", enums.MoveNodePositionChoices.values)
def test_api_documents_move_to_descendant(position):
"""
Moving a document to one of its descendants should return a validation error.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create a hierarchy: parent -> child -> grandchild
parent = factories.DocumentFactory(users=[(user, "owner")])
child = factories.DocumentFactory(parent=parent, users=[(user, "owner")])
grandchild = factories.DocumentFactory(parent=child, users=[(user, "owner")])
# Try moving parent to child (descendant)
response = client.post(
f"/api/v1.0/documents/{parent.id!s}/move/",
data={"target_document_id": str(child.id), "position": position},
)
assert response.status_code == 400
assert response.json() == {
"target_document_id": "Cannot move a document to its own descendant."
}
# Try moving parent to grandchild
response = client.post(
f"/api/v1.0/documents/{parent.id!s}/move/",
data={"target_document_id": str(grandchild.id), "position": position},
)
assert response.status_code == 400
assert response.json() == {
"target_document_id": "Cannot move a document to its own descendant."
}
# Try moving child to grandchild (still descendant)
response = client.post(
f"/api/v1.0/documents/{child.id!s}/move/",
data={"target_document_id": str(grandchild.id), "position": position},
)
assert response.status_code == 400
assert response.json() == {
"target_document_id": "Cannot move a document to its own descendant."
}
# Ensure documents have not moved
parent.refresh_from_db()
child.refresh_from_db()
grandchild.refresh_from_db()
assert parent.is_root() is True
assert child.is_child_of(parent) is True
assert grandchild.is_child_of(child) is True
@pytest.mark.parametrize(
"position",
[
enums.MoveNodePositionChoices.FIRST_CHILD,
enums.MoveNodePositionChoices.LAST_CHILD,
],
)
def test_api_documents_move_to_self(position):
"""
Moving a document to itself should return a validation error.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "owner")])
# Try moving document to itself
response = client.post(
f"/api/v1.0/documents/{document.id!s}/move/",
data={"target_document_id": str(document.id), "position": position},
)
assert response.status_code == 400
assert response.json() == {
"target_document_id": "Cannot move a document to its own descendant."
}
# Ensure document has not moved
document.refresh_from_db()
assert document.is_root() is True

View File

@@ -124,22 +124,3 @@ def test_api_documents_restore_authenticated_owner_expired():
assert response.status_code == 404
assert response.json() == {"detail": "Not found."}
def test_api_documents_restore_authenticated_owner_not_deleted():
"""Restoring a document that is not deleted should return a 400 error."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
response = client.post(f"/api/v1.0/documents/{document.id!s}/restore/")
assert response.status_code == 400
assert response.json() == {"detail": "This document is not deleted."}
document.refresh_from_db()
assert document.deleted_at is None
assert document.ancestors_deleted_at is None

View File

@@ -68,8 +68,8 @@ def test_api_documents_search_descendants_list_anonymous_public_standalone():
},
{
"abilities": child1.get_abilities(AnonymousUser()),
"ancestors_link_reach": child1.ancestors_link_reach,
"ancestors_link_role": child1.ancestors_link_role,
"ancestors_link_reach": document.link_reach,
"ancestors_link_role": document.link_role,
"computed_link_reach": child1.computed_link_reach,
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
@@ -91,8 +91,10 @@ def test_api_documents_search_descendants_list_anonymous_public_standalone():
},
{
"abilities": grand_child.get_abilities(AnonymousUser()),
"ancestors_link_reach": grand_child.ancestors_link_reach,
"ancestors_link_role": grand_child.ancestors_link_role,
"ancestors_link_reach": document.link_reach,
"ancestors_link_role": document.link_role
if (child1.link_reach == "public" and child1.link_role == "editor")
else document.link_role,
"computed_link_reach": "public",
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
@@ -114,8 +116,8 @@ def test_api_documents_search_descendants_list_anonymous_public_standalone():
},
{
"abilities": child2.get_abilities(AnonymousUser()),
"ancestors_link_reach": child2.ancestors_link_reach,
"ancestors_link_role": child2.ancestors_link_role,
"ancestors_link_reach": document.link_reach,
"ancestors_link_role": document.link_role,
"computed_link_reach": "public",
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
@@ -178,7 +180,7 @@ def test_api_documents_search_descendants_list_anonymous_public_parent():
# the search should include the parent document itself
"abilities": document.get_abilities(AnonymousUser()),
"ancestors_link_reach": "public",
"ancestors_link_role": document.ancestors_link_role,
"ancestors_link_role": grand_parent.link_role,
"computed_link_reach": document.computed_link_reach,
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
@@ -201,7 +203,7 @@ def test_api_documents_search_descendants_list_anonymous_public_parent():
{
"abilities": child1.get_abilities(AnonymousUser()),
"ancestors_link_reach": "public",
"ancestors_link_role": child1.ancestors_link_role,
"ancestors_link_role": grand_parent.link_role,
"computed_link_reach": child1.computed_link_reach,
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
@@ -247,7 +249,7 @@ def test_api_documents_search_descendants_list_anonymous_public_parent():
{
"abilities": child2.get_abilities(AnonymousUser()),
"ancestors_link_reach": "public",
"ancestors_link_role": child2.ancestors_link_role,
"ancestors_link_role": grand_parent.link_role,
"computed_link_reach": "public",
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
@@ -325,7 +327,7 @@ def test_api_documents_search_descendants_list_authenticated_unrelated_public_or
{
"abilities": child1.get_abilities(user),
"ancestors_link_reach": reach,
"ancestors_link_role": child1.ancestors_link_role,
"ancestors_link_role": document.link_role,
"computed_link_reach": child1.computed_link_reach,
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
@@ -348,7 +350,7 @@ def test_api_documents_search_descendants_list_authenticated_unrelated_public_or
{
"abilities": grand_child.get_abilities(user),
"ancestors_link_reach": reach,
"ancestors_link_role": grand_child.ancestors_link_role,
"ancestors_link_role": document.link_role,
"computed_link_reach": grand_child.computed_link_reach,
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
@@ -371,7 +373,7 @@ def test_api_documents_search_descendants_list_authenticated_unrelated_public_or
{
"abilities": child2.get_abilities(user),
"ancestors_link_reach": reach,
"ancestors_link_role": child2.ancestors_link_role,
"ancestors_link_role": document.link_role,
"computed_link_reach": child2.computed_link_reach,
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
@@ -435,7 +437,7 @@ def test_api_documents_search_descendants_list_authenticated_public_or_authentic
{
"abilities": child1.get_abilities(user),
"ancestors_link_reach": reach,
"ancestors_link_role": child1.ancestors_link_role,
"ancestors_link_role": grand_parent.link_role,
"computed_link_reach": child1.computed_link_reach,
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
@@ -458,7 +460,7 @@ def test_api_documents_search_descendants_list_authenticated_public_or_authentic
{
"abilities": grand_child.get_abilities(user),
"ancestors_link_reach": reach,
"ancestors_link_role": grand_child.ancestors_link_role,
"ancestors_link_role": grand_parent.link_role,
"computed_link_reach": grand_child.computed_link_reach,
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
@@ -481,7 +483,7 @@ def test_api_documents_search_descendants_list_authenticated_public_or_authentic
{
"abilities": child2.get_abilities(user),
"ancestors_link_reach": reach,
"ancestors_link_role": child2.ancestors_link_role,
"ancestors_link_role": grand_parent.link_role,
"computed_link_reach": child2.computed_link_reach,
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),

View File

@@ -14,7 +14,6 @@ import pytest
from rest_framework.test import APIClient
from core import factories, models
from core.services.ai_services.legacy import get_legacy_ai_service
from core.tests.documents.test_api_documents_ai_proxy import ( # pylint: disable=unused-import
ai_settings,
)
@@ -24,12 +23,6 @@ pytestmark = pytest.mark.django_db
# pylint: disable=unused-argument
@pytest.fixture(autouse=True)
def clear_openai_client_config():
"""Clear the configure_legacy_openai_client cache."""
get_legacy_ai_service.cache_clear()
def test_external_api_documents_ai_transform_not_allowed(
user_token, resource_server_backend, user_specific_sub
):
@@ -226,9 +219,7 @@ def test_external_api_documents_ai_translate_can_be_allowed(
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -250,7 +241,7 @@ def test_external_api_documents_ai_translate_can_be_allowed(
}
)
@pytest.mark.usefixtures("ai_settings")
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_external_api_documents_ai_proxy_can_be_allowed(
mock_stream, user_token, resource_server_backend, user_specific_sub
):

View File

@@ -10,23 +10,14 @@ from django.core.exceptions import ImproperlyConfigured
from django.test.utils import override_settings
import pytest
from mistralai import Mistral
from openai import OpenAI, OpenAIError
from pydantic_ai.models.mistral import MistralModel
from pydantic_ai.models.openai import OpenAIChatModel
from openai import OpenAIError
from pydantic_ai.ui.vercel_ai.request_types import TextUIPart, UIMessage
from core.services.ai_services.blocknote import (
from core.services.ai_services import (
BLOCKNOTE_TOOL_STRICT_PROMPT,
AIService,
configure_pydantic_model_provider,
convert_async_generator_to_sync,
)
from core.services.ai_services.legacy import (
LegacyAiServiceMistralClient,
LegacyAiServiceOpenAiClient,
get_legacy_ai_service,
)
pytestmark = pytest.mark.django_db
@@ -35,129 +26,35 @@ pytestmark = pytest.mark.django_db
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_BASE_URL = "http://example.com"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
yield
configure_pydantic_model_provider.cache_clear()
get_legacy_ai_service.cache_clear()
# -- AIService configure sdk--
# -- AIService.__init__ --
@pytest.mark.parametrize(
"setting_name, setting_value",
[
("OPENAI_SDK_BASE_URL", None),
("OPENAI_SDK_API_KEY", None),
("AI_BASE_URL", None),
("AI_API_KEY", None),
("AI_MODEL", None),
],
)
def test_ai_services_configure_open_ai_leagcy_client_missing_settings(
setting_name, setting_value, settings
):
"""
An exception must be raised if an expected settings is missing to configure the openai sdk.
"""
def test_services_ai_setting_missing(setting_name, setting_value, settings):
"""Setting should be set"""
setattr(settings, setting_name, setting_value)
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
LegacyAiServiceOpenAiClient()
def test_ai_services_configure_open_ai_leagcy_client(settings):
"""With all required settings the OpenAi legacy client should be configured."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
legacy_openai_client = LegacyAiServiceOpenAiClient()
assert isinstance(legacy_openai_client.client, OpenAI)
@pytest.mark.parametrize(
"setting_name, setting_value",
[
("MISTRAL_SDK_BASE_URL", None),
("MISTRAL_SDK_API_KEY", None),
("AI_MODEL", None),
],
)
def test_ai_services_configure_mistral_sdk_leagcy_client_missing_settings(
setting_name, setting_value, settings
):
"""
An exception must be raised if an expected settings is missing to configure the openai sdk.
"""
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
setattr(settings, setting_name, setting_value)
with pytest.raises(
ImproperlyConfigured,
match="Mistral sdk configuration not set",
):
LegacyAiServiceMistralClient()
def test_ai_services_configure_mistral_sdk_legacy_client(settings):
"""With all required settings the Mistral sdk legacy client should be configured."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
settings.MISTRAL_SDK_API_KEY = "mistreal-sdk-key"
settings.MISTRAL_SDK_BASE_URL = "https://mistral.base-url.com"
legacy_mistral_client = LegacyAiServiceMistralClient()
assert isinstance(legacy_mistral_client.client, Mistral)
def test_ai_services_configure_pydantic_ai_model_openai(settings):
"""When openai sdk settings are configured it should return an OpenAiChatModel."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
pydantic_ai_model = configure_pydantic_model_provider()
assert isinstance(pydantic_ai_model, OpenAIChatModel)
def test_ai_services_configure_pydantic_ai_model_mistral(settings):
"""When mistral sdk settings are configured is should return a MistralModel."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
settings.MISTRAL_SDK_API_KEY = "mistreal-sdk-key"
settings.MISTRAL_SDK_BASE_URL = "https://mistral.base-url.com"
pydantic_ai_model = configure_pydantic_model_provider()
assert isinstance(pydantic_ai_model, MistralModel)
def test_ai_services_configure_pydantic_ai_model_no_settings(settings):
"""When no settings are configured for a ai sdk it should raises an exception."""
settings.AI_MODEL = None
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
settings.MISTRAL_SDK_API_KEY = None
settings.MISTRAL_SDK_BASE_URL = None
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
configure_pydantic_model_provider()
AIService()
# -- AIService.transform --
@@ -176,7 +73,7 @@ def test_services_ai_client_error(mock_create):
OpenAIError,
match="Mocked client error",
):
get_legacy_ai_service().transform("hello", "prompt")
AIService().transform("hello", "prompt")
@override_settings(
@@ -194,7 +91,7 @@ def test_services_ai_client_invalid_response(mock_create):
RuntimeError,
match="AI response does not contain an answer",
):
get_legacy_ai_service().transform("hello", "prompt")
AIService().transform("hello", "prompt")
@override_settings(
@@ -208,7 +105,7 @@ def test_services_ai_success(mock_create):
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
response = get_legacy_ai_service().transform("hello", "prompt")
response = AIService().transform("hello", "prompt")
assert response == {"answer": "Salut"}
@@ -224,7 +121,7 @@ def test_services_ai_translate_success(mock_create):
choices=[MagicMock(message=MagicMock(content="Bonjour"))]
)
response = get_legacy_ai_service().translate("<p>Hello</p>", "fr")
response = AIService().translate("<p>Hello</p>", "fr")
assert response == {"answer": "Bonjour"}
call_args = mock_create.call_args
@@ -240,7 +137,7 @@ def test_services_ai_translate_unknown_language(mock_create):
choices=[MagicMock(message=MagicMock(content="Translated"))]
)
response = get_legacy_ai_service().translate("<p>Hello</p>", "xx-unknown")
response = AIService().translate("<p>Hello</p>", "xx-unknown")
assert response == {"answer": "Translated"}
call_args = mock_create.call_args
@@ -551,7 +448,7 @@ def test_services_ai_stream_defaults_to_sync(mock_build, monkeypatch):
# -- AIService._build_async_stream --
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream(mock_adapter_cls):
"""_build_async_stream should build the pydantic-ai streaming pipeline."""
@@ -580,7 +477,7 @@ def test_services_ai_build_async_stream(mock_adapter_cls):
mock_adapter_instance.encode_stream.assert_called_once()
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_with_tool_definitions(mock_adapter_cls):
"""_build_async_stream should build an ExternalToolset when
toolDefinitions are present in the request."""
@@ -617,7 +514,7 @@ def test_services_ai_build_async_stream_with_tool_definitions(mock_adapter_cls):
assert len(call_kwargs["toolsets"]) == 1
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_with_tool_definitions_required_system_prompt(
mock_adapter_cls,
):
@@ -660,8 +557,8 @@ def test_services_ai_build_async_stream_with_tool_definitions_required_system_pr
assert mock_run_input.messages[0].parts[0].text == BLOCKNOTE_TOOL_STRICT_PROMPT
@patch("core.services.ai_services.blocknote.Agent")
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.Agent")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_langfuse_enabled(
mock_adapter_cls, mock_agent_cls, settings
):

View File

@@ -12,14 +12,13 @@ import pytest
import responses
from requests import HTTPError
from core import factories, models
from core import factories, models, utils
from core.services.search_indexers import (
BaseDocumentIndexer,
FindDocumentIndexer,
get_document_indexer,
get_visited_document_ids_of,
)
from core.utils.yjs import base64_yjs_to_text
pytestmark = pytest.mark.django_db
@@ -200,7 +199,7 @@ def test_services_search_indexers_serialize_document_returns_expected_json():
"depth": 1,
"path": document.path,
"numchild": 1,
"content": base64_yjs_to_text(document.content),
"content": utils.base64_yjs_to_text(document.content),
"created_at": document.created_at.isoformat(),
"updated_at": document.updated_at.isoformat(),
"reach": document.link_reach,

View File

@@ -8,18 +8,7 @@ from django.core.cache import cache
import pycrdt
import pytest
from core import factories
from core.utils.dicts import get_value_by_pattern
from core.utils.paths import get_ancestor_to_descendants_map
from core.utils.users import (
get_users_sharing_documents_with_cache_key,
users_sharing_documents_with,
)
from core.utils.yjs import (
base64_yjs_to_text,
base64_yjs_to_xml,
extract_attachments,
)
from core import factories, utils
pytestmark = pytest.mark.django_db
@@ -45,12 +34,12 @@ TEST_BASE64_STRING = (
def test_utils_base64_yjs_to_text():
"""Test extract text from saved yjs document"""
assert base64_yjs_to_text(TEST_BASE64_STRING) == "Hello w or ld"
assert utils.base64_yjs_to_text(TEST_BASE64_STRING) == "Hello w or ld"
def test_utils_base64_yjs_to_xml():
"""Test extract xml from saved yjs document"""
content = base64_yjs_to_xml(TEST_BASE64_STRING)
content = utils.base64_yjs_to_xml(TEST_BASE64_STRING)
assert (
'<heading textAlignment="left" level="1"><italic>Hello</italic></heading>'
in content
@@ -90,13 +79,13 @@ def test_utils_extract_attachments():
update = ydoc.get_update()
base64_string = base64.b64encode(update).decode("utf-8")
# image_key2 is missing the "/media/" part and shouldn't get extracted
assert extract_attachments(base64_string) == [image_key1, image_key3]
assert utils.extract_attachments(base64_string) == [image_key1, image_key3]
def test_utils_get_ancestor_to_descendants_map_single_path():
"""Test ancestor mapping of a single path."""
paths = ["000100020005"]
result = get_ancestor_to_descendants_map(paths, steplen=4)
result = utils.get_ancestor_to_descendants_map(paths, steplen=4)
assert result == {
"0001": {"000100020005"},
@@ -108,7 +97,7 @@ def test_utils_get_ancestor_to_descendants_map_single_path():
def test_utils_get_ancestor_to_descendants_map_multiple_paths():
"""Test ancestor mapping of multiple paths with shared prefixes."""
paths = ["000100020005", "00010003"]
result = get_ancestor_to_descendants_map(paths, steplen=4)
result = utils.get_ancestor_to_descendants_map(paths, steplen=4)
assert result == {
"0001": {"000100020005", "00010003"},
@@ -130,10 +119,10 @@ def test_utils_users_sharing_documents_with_cache_miss():
factories.UserDocumentAccessFactory(user=user2, document=doc1)
factories.UserDocumentAccessFactory(user=user3, document=doc2)
cache_key = get_users_sharing_documents_with_cache_key(user1)
cache_key = utils.get_users_sharing_documents_with_cache_key(user1)
cache.delete(cache_key)
result = users_sharing_documents_with(user1)
result = utils.users_sharing_documents_with(user1)
assert user2.id in result
@@ -150,12 +139,12 @@ def test_utils_users_sharing_documents_with_cache_hit():
factories.UserDocumentAccessFactory(user=user1, document=doc1)
factories.UserDocumentAccessFactory(user=user2, document=doc1)
cache_key = get_users_sharing_documents_with_cache_key(user1)
cache_key = utils.get_users_sharing_documents_with_cache_key(user1)
test_cached_data = {user2.id: "2025-02-10"}
cache.set(cache_key, test_cached_data, 86400)
result = users_sharing_documents_with(user1)
result = utils.users_sharing_documents_with(user1)
assert result == test_cached_data
@@ -167,7 +156,7 @@ def test_utils_users_sharing_documents_with_cache_invalidation_on_create():
doc1 = factories.DocumentFactory()
# Pre-populate cache
cache_key = get_users_sharing_documents_with_cache_key(user1)
cache_key = utils.get_users_sharing_documents_with_cache_key(user1)
cache.set(cache_key, {}, 86400)
# Verify cache exists
@@ -193,7 +182,7 @@ def test_utils_users_sharing_documents_with_cache_invalidation_on_delete():
doc_access = factories.UserDocumentAccessFactory(user=user1, document=doc1)
cache_key = get_users_sharing_documents_with_cache_key(user1)
cache_key = utils.get_users_sharing_documents_with_cache_key(user1)
cache.set(cache_key, {user2.id: "2025-02-10"}, 86400)
assert cache.get(cache_key) is not None
@@ -207,10 +196,10 @@ def test_utils_users_sharing_documents_with_empty_result():
"""Test when user is not sharing any documents."""
user1 = factories.UserFactory()
cache_key = get_users_sharing_documents_with_cache_key(user1)
cache_key = utils.get_users_sharing_documents_with_cache_key(user1)
cache.delete(cache_key)
result = users_sharing_documents_with(user1)
result = utils.users_sharing_documents_with(user1)
assert result == {}
@@ -221,7 +210,7 @@ def test_utils_users_sharing_documents_with_empty_result():
def test_utils_get_value_by_pattern_matching_key():
"""Test extracting value from a dictionary with a matching key pattern."""
data = {"title.extension": "Bonjour", "id": 1, "content": "test"}
result = get_value_by_pattern(data, r"^title\.")
result = utils.get_value_by_pattern(data, r"^title\.")
assert set(result) == {"Bonjour"}
@@ -229,7 +218,7 @@ def test_utils_get_value_by_pattern_matching_key():
def test_utils_get_value_by_pattern_multiple_matches():
"""Test that all matching keys are returned."""
data = {"title.extension_1": "Bonjour", "title.extension_2": "Hello", "id": 1}
result = get_value_by_pattern(data, r"^title\.")
result = utils.get_value_by_pattern(data, r"^title\.")
assert set(result) == {
"Bonjour",
@@ -240,7 +229,7 @@ def test_utils_get_value_by_pattern_multiple_matches():
def test_utils_get_value_by_pattern_multiple_extensions():
"""Test that all matching keys are returned."""
data = {"title.extension_1.extension_2": "Bonjour", "id": 1}
result = get_value_by_pattern(data, r"^title\.")
result = utils.get_value_by_pattern(data, r"^title\.")
assert set(result) == {"Bonjour"}
@@ -248,6 +237,6 @@ def test_utils_get_value_by_pattern_multiple_extensions():
def test_utils_get_value_by_pattern_no_match():
"""Test that empty list is returned when no key matches the pattern."""
data = {"name": "Test", "id": 1}
result = get_value_by_pattern(data, r"^title\.")
result = utils.get_value_by_pattern(data, r"^title\.")
assert result == []

View File

@@ -1,89 +0,0 @@
"""Tests for the create_tree_node_with_retry utils."""
from unittest import mock
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError
import pytest
from core.factories import UserFactory
from core.models import Document
from core.utils.treebeard import _is_tree_path_collision, create_tree_node_with_retry
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"exc",
[
DjangoValidationError({"path": "not unique"}),
IntegrityError("impress_document_path_key"),
],
)
def test_utils_create_tree_node_with_retry_exceed_max_attempts(settings, exc):
"""Test exceeding the max attempts should reraise the exception."""
settings.TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS = 2
create_fn = mock.MagicMock()
create_fn.side_effect = exc
with (
pytest.raises(exc.__class__),
mock.patch(
"core.utils.treebeard._is_tree_path_collision"
) as mock__is_tree_path_collision,
):
mock__is_tree_path_collision.side_effect = _is_tree_path_collision
create_tree_node_with_retry(create_fn)
mock__is_tree_path_collision.assert_called()
assert mock__is_tree_path_collision.call_count == 2
assert create_fn.call_count == 2
@pytest.mark.parametrize(
"exc",
[
DjangoValidationError({"foo": "bar"}),
IntegrityError("not handled"),
],
)
def test_utils_create_tree_node_with_retry_exceed_exception_not_handled(settings, exc):
"""Test with an exception not handled should return reraise it immediatly."""
settings.TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS = 2
create_fn = mock.MagicMock()
create_fn.side_effect = exc
with (
pytest.raises(exc.__class__),
mock.patch(
"core.utils.treebeard._is_tree_path_collision"
) as mock__is_tree_path_collision,
):
mock__is_tree_path_collision.side_effect = _is_tree_path_collision
create_tree_node_with_retry(create_fn)
mock__is_tree_path_collision.assert_called()
assert mock__is_tree_path_collision.call_count == 1
assert create_fn.call_count == 1
def test_utils_create_tree_node_with_retry_success():
"""Test executing successfully the create_fn callback."""
user = UserFactory()
document = create_tree_node_with_retry(
lambda: Document.add_root(
creator=user,
title="success",
)
)
assert isinstance(document, Document)
assert document.title == "success"
assert document.path is not None

View File

@@ -2,7 +2,7 @@
Unit tests for the filter_root_paths utility function.
"""
from core.utils.paths import filter_descendants
from core.utils import filter_descendants
def test_utils_filter_descendants_success():

View File

@@ -4,8 +4,7 @@ from django.utils import timezone
import pytest
from core import factories
from core.utils.users import users_sharing_documents_with
from core import factories, utils
pytestmark = pytest.mark.django_db
@@ -55,7 +54,7 @@ def test_utils_users_sharing_documents_with():
doc_3_pierre_2.created_at = yesterday
doc_3_pierre_2.save()
shared_map = users_sharing_documents_with(user)
shared_map = utils.users_sharing_documents_with(user)
assert shared_map == {
pierre_1.id: last_week,

170
src/backend/core/utils.py Normal file
View File

@@ -0,0 +1,170 @@
"""Utils for the core app."""
import base64
import logging
import re
import time
from collections import defaultdict
from django.core.cache import cache
from django.db import models as db
from django.db.models import Subquery
import pycrdt
from bs4 import BeautifulSoup
from core import enums, models
logger = logging.getLogger(__name__)
def get_value_by_pattern(data, pattern):
"""
Get all values from keys matching a regex pattern in a dictionary.
Args:
data (dict): Source dictionary to search
pattern (str): Regex pattern to match against keys
Returns:
list: List of values for all matching keys, empty list if no matches
Example:
>>> get_value_by_pattern({"title.fr": "Bonjour", "id": 1}, r"^title\\.")
["Bonjour"]
>>> get_value_by_pattern({"title.fr": "Bonjour", "title.en": "Hello"}, r"^title\\.")
["Bonjour", "Hello"]
"""
regex = re.compile(pattern)
return [value for key, value in data.items() if regex.match(key)]
def get_ancestor_to_descendants_map(paths, steplen):
"""
Given a list of document paths, return a mapping of ancestor_path -> set of descendant_paths.
Each path is assumed to use materialized path format with fixed-length segments.
Args:
paths (list of str): List of full document paths.
steplen (int): Length of each path segment.
Returns:
dict[str, set[str]]: Mapping from ancestor path to its descendant paths (including itself).
"""
ancestor_map = defaultdict(set)
for path in paths:
for i in range(steplen, len(path) + 1, steplen):
ancestor = path[:i]
ancestor_map[ancestor].add(path)
return ancestor_map
def filter_descendants(paths, root_paths, skip_sorting=False):
"""
Filters paths to keep only those that are descendants of any path in root_paths.
A path is considered a descendant of a root path if it starts with the root path.
If `skip_sorting` is not set to True, the function will sort both lists before
processing because both `paths` and `root_paths` need to be in lexicographic order
before going through the algorithm.
Args:
paths (iterable of str): List of paths to be filtered.
root_paths (iterable of str): List of paths to check as potential prefixes.
skip_sorting (bool): If True, assumes both `paths` and `root_paths` are already sorted.
Returns:
list of str: A list of sorted paths that are descendants of any path in `root_paths`.
"""
results = []
i = 0
n = len(root_paths)
if not skip_sorting:
paths.sort()
root_paths.sort()
for path in paths:
# Try to find a matching prefix in the sorted accessible paths
while i < n:
if path.startswith(root_paths[i]):
results.append(path)
break
if root_paths[i] < path:
i += 1
else:
# If paths[i] > path, no need to keep searching
break
return results
def base64_yjs_to_xml(base64_string):
"""Extract xml from base64 yjs document."""
decoded_bytes = base64.b64decode(base64_string)
# uint8_array = bytearray(decoded_bytes)
doc = pycrdt.Doc()
doc.apply_update(decoded_bytes)
return str(doc.get("document-store", type=pycrdt.XmlFragment))
def base64_yjs_to_text(base64_string):
"""Extract text from base64 yjs document."""
blocknote_structure = base64_yjs_to_xml(base64_string)
soup = BeautifulSoup(blocknote_structure, "lxml-xml")
return soup.get_text(separator=" ", strip=True)
def extract_attachments(content):
"""Helper method to extract media paths from a document's content."""
if not content:
return []
xml_content = base64_yjs_to_xml(content)
return re.findall(enums.MEDIA_STORAGE_URL_EXTRACT, xml_content)
def get_users_sharing_documents_with_cache_key(user):
"""Generate a unique cache key for each user."""
return f"users_sharing_documents_with_{user.id}"
def users_sharing_documents_with(user):
"""
Returns a map of users sharing documents with the given user,
sorted by last shared date.
"""
start_time = time.time()
cache_key = get_users_sharing_documents_with_cache_key(user)
cached_result = cache.get(cache_key)
if cached_result is not None:
elapsed = time.time() - start_time
logger.info(
"users_sharing_documents_with cache hit for user %s (took %.3fs)",
user.id,
elapsed,
)
return cached_result
user_docs_qs = models.DocumentAccess.objects.filter(user=user).values_list(
"document_id", flat=True
)
shared_qs = (
models.DocumentAccess.objects.filter(document_id__in=Subquery(user_docs_qs))
.exclude(user=user)
.values("user")
.annotate(last_shared=db.Max("created_at"))
)
result = {item["user"]: item["last_shared"] for item in shared_qs}
cache.set(cache_key, result, 86400) # Cache for 1 day
elapsed = time.time() - start_time
logger.info(
"users_sharing_documents_with cache miss for user %s (took %.3fs)",
user.id,
elapsed,
)
return result

View File

@@ -1 +0,0 @@
"""Core utilities package."""

View File

@@ -1,24 +0,0 @@
"""Dictionary utility functions."""
import re
def get_value_by_pattern(data, pattern):
"""
Get all values from keys matching a regex pattern in a dictionary.
Args:
data (dict): Source dictionary to search
pattern (str): Regex pattern to match against keys
Returns:
list: List of values for all matching keys, empty list if no matches
Example:
>>> get_value_by_pattern({"title.fr": "Bonjour", "id": 1}, r"^title\\.")
["Bonjour"]
>>> get_value_by_pattern({"title.fr": "Bonjour", "title.en": "Hello"}, r"^title\\.")
["Bonjour", "Hello"]
"""
regex = re.compile(pattern)
return [value for key, value in data.items() if regex.match(key)]

View File

@@ -1,63 +0,0 @@
"""Path and tree structure utilities."""
from collections import defaultdict
def get_ancestor_to_descendants_map(paths, steplen):
"""
Given a list of document paths, return a mapping of ancestor_path -> set of descendant_paths.
Each path is assumed to use materialized path format with fixed-length segments.
Args:
paths (list of str): List of full document paths.
steplen (int): Length of each path segment.
Returns:
dict[str, set[str]]: Mapping from ancestor path to its descendant paths (including itself).
"""
ancestor_map = defaultdict(set)
for path in paths:
for i in range(steplen, len(path) + 1, steplen):
ancestor = path[:i]
ancestor_map[ancestor].add(path)
return ancestor_map
def filter_descendants(paths, root_paths, skip_sorting=False):
"""
Filters paths to keep only those that are descendants of any path in root_paths.
A path is considered a descendant of a root path if it starts with the root path.
If `skip_sorting` is not set to True, the function will sort both lists before
processing because both `paths` and `root_paths` need to be in lexicographic order
before going through the algorithm.
Args:
paths (iterable of str): List of paths to be filtered.
root_paths (iterable of str): List of paths to check as potential prefixes.
skip_sorting (bool): If True, assumes both `paths` and `root_paths` are already sorted.
Returns:
list of str: A list of sorted paths that are descendants of any path in `root_paths`.
"""
results = []
i = 0
n = len(root_paths)
if not skip_sorting:
paths.sort()
root_paths.sort()
for path in paths:
# Try to find a matching prefix in the sorted accessible paths
while i < n:
if path.startswith(root_paths[i]):
results.append(path)
break
if root_paths[i] < path:
i += 1
else:
# If paths[i] > path, no need to keep searching
break
return results

View File

@@ -1,56 +0,0 @@
"""Treebeard path collision handling utilities."""
import logging
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError, transaction
logger = logging.getLogger(__name__)
def _is_tree_path_collision(exc):
"""Return True when `exc` is caused by a Document.path uniqueness conflict.
Treebeard computes the materialized path by reading the current siblings;
under concurrency two callers may compute the same value. Depending on
timing this surfaces either as:
- `django.core.exceptions.ValidationError` raised by `full_clean()` /
`validate_unique()` before the INSERT (BaseModel.save calls full_clean),
- or `IntegrityError` from the database unique index when the validate
step misses the conflict.
"""
if isinstance(exc, DjangoValidationError):
message_dict = getattr(exc, "message_dict", None)
if message_dict is not None:
return "path" in message_dict
return "path" in str(exc).lower()
# search in the IntegrityError exception
return "impress_document_path_key" in str(exc).lower()
def create_tree_node_with_retry(create_fn):
"""Run `create_fn` in a fresh atomic block, retrying on path collisions.
The Document.path field carries a unique constraint, which is the source of
truth that prevents duplicate paths. On collision we let the failed
transaction roll back, and call `create_fn` again so treebeard recomputes
the path from the latest state.
"""
max_attempts = settings.TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS
for attempt in range(max_attempts):
try:
with transaction.atomic():
return create_fn()
except (IntegrityError, DjangoValidationError) as exc:
if not _is_tree_path_collision(exc) or attempt == max_attempts - 1:
raise
logger.info(
"tree path collision on attempt %d/%d, retrying",
attempt + 1,
max_attempts,
)
raise RuntimeError("create_tree_node_with_retry exited without result")

View File

@@ -1,55 +0,0 @@
"""User sharing cache utilities."""
import logging
import time
from django.core.cache import cache
from django.db import models as db
from django.db.models import Subquery
from core import models
logger = logging.getLogger(__name__)
def get_users_sharing_documents_with_cache_key(user):
"""Generate a unique cache key for each user."""
return f"users_sharing_documents_with_{user.id}"
def users_sharing_documents_with(user):
"""
Returns a map of users sharing documents with the given user,
sorted by last shared date.
"""
start_time = time.time()
cache_key = get_users_sharing_documents_with_cache_key(user)
cached_result = cache.get(cache_key)
if cached_result is not None:
elapsed = time.time() - start_time
logger.info(
"users_sharing_documents_with cache hit for user %s (took %.3fs)",
user.id,
elapsed,
)
return cached_result
user_docs_qs = models.DocumentAccess.objects.filter(user=user).values_list(
"document_id", flat=True
)
shared_qs = (
models.DocumentAccess.objects.filter(document_id__in=Subquery(user_docs_qs))
.exclude(user=user)
.values("user")
.annotate(last_shared=db.Max("created_at"))
)
result = {item["user"]: item["last_shared"] for item in shared_qs}
cache.set(cache_key, result, 86400) # Cache for 1 day
elapsed = time.time() - start_time
logger.info(
"users_sharing_documents_with cache miss for user %s (took %.3fs)",
user.id,
elapsed,
)
return result

View File

@@ -1,36 +0,0 @@
"""Yjs document conversion utilities."""
import base64
import re
import pycrdt
from bs4 import BeautifulSoup
from core import enums
def base64_yjs_to_xml(base64_string):
"""Extract xml from base64 yjs document."""
decoded_bytes = base64.b64decode(base64_string)
doc = pycrdt.Doc()
doc.apply_update(decoded_bytes)
return str(doc.get("document-store", type=pycrdt.XmlFragment))
def base64_yjs_to_text(base64_string):
"""Extract text from base64 yjs document."""
blocknote_structure = base64_yjs_to_xml(base64_string)
soup = BeautifulSoup(blocknote_structure, "lxml-xml")
return soup.get_text(separator=" ", strip=True)
def extract_attachments(content):
"""Helper method to extract media paths from a document's content."""
if not content:
return []
xml_content = base64_yjs_to_xml(content)
return re.findall(enums.MEDIA_STORAGE_URL_EXTRACT, xml_content)

View File

@@ -130,12 +130,6 @@ class Base(Configuration):
default=50, environ_name="SEARCH_INDEXER_QUERY_LIMIT", environ_prefix=None
)
MEDIA_AUTH_ORIGINAL_URL_HEADER = values.Value(
default="HTTP_X_ORIGINAL_URL",
environ_name="MEDIA_AUTH_ORIGINAL_URL_HEADER",
environ_prefix=None,
)
# Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(DATA_DIR, "static")
@@ -808,30 +802,8 @@ class Base(Configuration):
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
MISTRAL_SDK_BASE_URL = values.Value(
None, environ_name="MISTRAL_SDK_BASE_URL", environ_prefix=None
)
MISTRAL_SDK_API_KEY = SecretFileValue(
None, environ_name="MISTRAL_SDK_API_KEY", environ_prefix=None
)
OPENAI_SDK_API_KEY = SecretFileValue(
default=SecretFileValue( # retrocompatibility
None,
environ_name="AI_API_KEY",
environ_prefix=None,
),
environ_name="OPENAI_SDK_API_KEY",
environ_prefix=None,
)
OPENAI_SDK_BASE_URL = values.Value(
default=values.Value( # retrocompatibility
None, environ_name="AI_BASE_URL", environ_prefix=None
),
environ_name="OPENAI_SDK_BASE_URL",
environ_prefix=None,
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_BOT = values.DictValue(
default={
"name": _("Docs AI"),
@@ -1081,12 +1053,6 @@ class Base(Configuration):
60 * 60 * 24, environ_name="CONTENT_METADATA_CACHE_TIMEOUT", environ_prefix=None
)
TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS = values.IntegerValue(
10,
environ_name="TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS",
environ_prefix=None,
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
@@ -1177,11 +1143,6 @@ class Base(Configuration):
}
)
if cls.OPENAI_SDK_API_KEY and cls.MISTRAL_SDK_API_KEY:
raise ValueError(
"Both OPENAI_SDK and MISTRAL_SDK parameters can not be set simultaneously."
)
class Build(Base):
"""Settings used when the application is built.

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Titouroù personel"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Aotreoù"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Deiziadoù a-bouez"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Gwezennadur"
@@ -62,24 +62,24 @@ msgstr "Kuzhet"
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -375,151 +375,151 @@ msgstr "Restr"
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Digeriñ"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Berechtigungen"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Wichtige Termine"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Import-Job erstellt und in der Warteschlange."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Baumstruktur"
@@ -62,24 +62,24 @@ msgstr "Maskiert"
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Dies ist ein Pflichtfeld."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Der Zugriff auf den Link '%(link_reach)s' ist aufgrund der Konfiguration übergeordneter Dokumente nicht erlaubt."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -149,15 +149,15 @@ msgstr "Rechts"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "ID"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "Primärschlüssel für den Datensatz als UUID"
msgstr "primärer Schlüssel für den Datensatz als UUID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "Erstellt am"
msgstr "Erstellt"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
@@ -375,151 +375,151 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Öffnen"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Dokument um Zugriff bitten"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Dokumentenabfragen"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Dieser Benutzer hat bereits um Zugang zu diesem Dokument gebeten."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} möchte Zugriff auf ein Dokument erhalten!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} möchte auf das folgende Dokument zugreifen:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} bittet um Zugang zum Dokument: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Thread"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Threads"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Gast"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Kommentar"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Kommentare"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reaktion"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reaktionen"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Προσωπικές πληροφορίες"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Δικαιώματα"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Σημαντικές ημερομηνίες"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Η εργασία εισαγωγής δημιουργήθηκε και μπήκε στην ουρά."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Επεξεργασία επιλεγμένων συμφωνιών χρηστών"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Δομή δέντρου"
@@ -62,24 +62,24 @@ msgstr "Με κάλυψη"
msgid "Favorite"
msgstr "Αγαπημένο"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ένα νέο έγγραφο δημιουργήθηκε εκ μέρους σας!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Σας παραχωρήθηκε η ιδιοκτησία ενός νέου εγγράφου:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Αυτό το πεδίο είναι υποχρεωτικό."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Η εμβέλεια συνδέσμου '%(link_reach)s' δεν επιτρέπεται βάσει της διαμόρφωσης του γονικού εγγράφου."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "αντίγραφο του {title}"
@@ -382,151 +382,151 @@ msgstr "Έγγραφο"
msgid "Documents"
msgstr "Έγγραφα"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Έγγραφο χωρίς τίτλο"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Άνοιγμα"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "Ο/Η {name} μοιράστηκε ένα έγγραφο μαζί σας!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Ο/Η {name} σας προσκάλεσε με τον ρόλο \"{role}\" στο ακόλουθο έγγραφο:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "Ο/Η {name} μοιράστηκε ένα έγγραφο μαζί σας: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Ίχνος συνδέσμου εγγράφου/χρήστη"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Ίχνη συνδέσμου εγγράφου/χρήστη"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Ένα ίχνος συνδέσμου υπάρχει ήδη για αυτό το έγγραφο/χρήστη."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Αγαπημένο έγγραφο"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Αγαπημένα έγγραφα"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Αυτό το έγγραφο στοχεύεται ήδη από μια σχέση αγαπημένου για τον ίδιο χρήστη."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Σχέση εγγράφου/χρήστη"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Σχέσεις εγγράφου/χρήστη"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Αυτός ο χρήστης συμμετέχει ήδη σε αυτό το έγγραφο."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Αυτή η ομάδα συμμετέχει ήδη σε αυτό το έγγραφο."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Πρέπει να οριστεί είτε χρήστης είτε ομάδα, όχι και τα δύο."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Αίτημα πρόσβασης σε έγγραφο"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Αιτήματα πρόσβασης σε έγγραφα"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Αυτός ο χρήστης έχει ήδη ζητήσει πρόσβαση σε αυτό το έγγραφο."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "Ο/Η {name} θα ήθελε πρόσβαση σε ένα έγγραφο!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "Ο/Η {name} θα ήθελε πρόσβαση στο ακόλουθο έγγραφο:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "Ο/Η {name} ζητά πρόσβαση στο έγγραφο: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Νήμα"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Νήματα"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Ανώνυμος"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Σχόλιο"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Σχόλια"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Αυτό το emoji έχει χρησιμοποιηθεί ήδη ως αντίδραση σε αυτό το σχόλιο."
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Αντίδραση"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Αντιδράσεις"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "διεύθυνση email"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Πρόσκληση σε έγγραφο"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Προσκλήσεις εγγράφου"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Αυτό το email σχετίζεται ήδη με έναν εγγεγραμμένο χρήστη."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Τεχνητή Νοημοσύνη (AI) Docs"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Información Personal"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permisos"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Fechas importantes"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Estructura en árbol"
@@ -62,24 +62,24 @@ msgstr "Enmascarado"
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -375,151 +375,151 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Ya existe una traza de enlace para este documento/usuario."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Thread"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Threads"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Anónimo"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Comentario"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Comentarios"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reacción"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reacciones"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permissions"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Tâche d'importation créée et mise en file d'attente."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Traiter les rapprochements de l'utilisateur sélectionné"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Arborescence"
@@ -62,24 +62,24 @@ msgstr "Masqué"
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -382,151 +382,151 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Ouvrir"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} a partagé un document avec vous : {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ce document est déjà un favori de cet utilisateur."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Conversation"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Conversations"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Anonyme"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Commentaire"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Commentaires"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Cet émoji a déjà été réagi à ce commentaire."
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Réaction"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Réactions"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs IA"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Informazioni personali"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permessi"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Date importanti"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Struttura ad albero"
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -375,151 +375,151 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Apri"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha condiviso un documento con te: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Persoonlijke informatie"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Machtigingen"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Belangrijke data"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Import taak gemaakt en in de wachtrij geplaatst."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Verwerk geselecteerde gebruikers samenvoeging"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Boomstructuur"
@@ -62,24 +62,24 @@ msgstr "Gemaskeerd"
msgid "Favorite"
msgstr "Favoriet"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -382,151 +382,151 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Open"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met u gedeeld!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} heeft een document met u gedeeld: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Een link bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "De gebruiker bestaat al in dit document."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Document verzoekt om toegangen"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Kanaal"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Kanalen"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Anoniem"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Reactie"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Reacties"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Deze emoji is al op deze opmerking gereageerd."
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reactie"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reacties"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Informações Pessoais"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permissões"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Datas importantes"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Estrutura de árvore"
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Разрешения"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Задание по импорту создано и поставлено в очередь."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Обработка выбранных пользовательских сверок"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Древовидная структура"
@@ -62,24 +62,24 @@ msgstr "Скрытый"
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -382,151 +382,151 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Открыть"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Обсуждение"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Обсуждения"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Аноним"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Комментарий"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Комментарии"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Этот эмодзи уже использован в этом комментарии."
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Реакция"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Реакции"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs ИИ"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Osebni podatki"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Dovoljenja"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Pomembni datumi"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Drevesna struktura"
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Odpri"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Personuppgifter"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Behörigheter"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Viktiga datum"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Öppna"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Дозволи"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Завдання імпорту створено і поставлено в чергу."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Обробити обрані узгодження користувача"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Ієрархічна структура"
@@ -62,24 +62,24 @@ msgstr "Приховано"
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -382,151 +382,151 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Відкрити"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Обговорення"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Обговорення"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Анонім"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Коментар"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Коментарі"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Цим емодзі вже відреагували на цей коментар."
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Реакція"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Реакції"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs ШІ"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "個人資訊"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "權限"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "重要日期"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "樹狀結構"
@@ -62,24 +62,24 @@ msgstr "已隱藏"
msgid "Favorite"
msgstr "我的最愛"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "已代表您建立新文件!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "您已獲得新文件的所有權:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "此欄位為必填。"
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "根據父文件設定,不允許連結範圍「%(link_reach)s」。"
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -375,151 +375,151 @@ msgstr "文件"
msgid "Documents"
msgstr "文件"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "未命名文件"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "開啟"
#: build/lib/core/models.py:1383 core/models.py:1383
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 與您分享了一份文件!"
#: build/lib/core/models.py:1387 core/models.py:1387
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀請您以「{role}」角色參與以下文件:"
#: build/lib/core/models.py:1393 core/models.py:1393
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 與您分享了一份文件:{title}"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:1501 core/models.py:1501
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "此文件/使用者已存在連結追蹤。"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "文件收藏"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "文件收藏"
#: build/lib/core/models.py:1531 core/models.py:1531
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "此使用者已將此文件加入收藏。"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "此使用者已在此文件中。"
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "此團隊已在此文件中。"
#: build/lib/core/models.py:1572 core/models.py:1572
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "必須設定使用者或團隊其中之一,不能同時設定兩者。"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "此使用者已要求過存取此文件的權限。"
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 想要存取文件!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 想要存取以下文件:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} 正要求存取文件:{title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "對話串"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "對話串"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "匿名"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "評論"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "評論"
#: build/lib/core/models.py:1940 core/models.py:1940
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "此評論已標記過此表情符號。"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "回應"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "回應"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "電子郵件地址"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "文件邀請"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "文件邀請"
#: build/lib/core/models.py:1995 core/models.py:1995
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "此電子郵件地址已與已註冊使用者關聯。"
#: build/lib/impress/settings.py:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "5.0.0"
version = "4.8.6"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -26,7 +26,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.14.3",
"boto3==1.42.93",
"boto3==1.42.59",
"Brotli==1.2.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
@@ -34,39 +34,37 @@ dependencies = [
"django-countries==8.2.0",
"django-csp==4.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.26",
"django-lasuite[all]==0.0.24",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django<6.0.0",
"django-treebeard<5.0.0",
"djangorestframework==3.17.1",
"djangorestframework==3.16.1",
"django-waffle==5.0.0",
"drf_spectacular==0.29.0",
"dockerflow==2026.3.4",
"dockerflow==2026.1.26",
"easy_thumbnails==2.10.1",
"emoji==2.15.0",
"factory_boy==3.3.3",
"gunicorn==25.3.0",
"gunicorn==25.1.0",
"jsonschema==4.26.0",
"langfuse==3.11.2",
"lxml==6.1.0",
"markdown==3.10.2",
"mistralai==1.12.4",
"mozilla-django-oidc==5.0.2",
"nested-multipart-parser==1.6.0",
"openai==2.32.0",
"openai==2.24.0",
"psycopg[binary,pool]==3.3.3",
"pycrdt==0.12.50",
"pydantic==2.13.3",
"pycrdt==0.12.47",
"pydantic==2.12.5",
"pydantic-ai-slim[openai,logfire,web]==1.58.0",
"PyJWT==2.12.1",
"PyJWT==2.12.0",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.33.1",
"sentry-sdk==2.58.0",
"uvicorn==0.45.0",
"requests==2.33.0",
"sentry-sdk==2.53.0",
"uvicorn==0.41.0",
"whitenoise==6.12.0",
]
@@ -80,21 +78,21 @@ dependencies = [
dev = [
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2026.4.14",
"drf-spectacular-sidecar==2026.3.1",
"freezegun==1.5.5",
"ipdb==0.13.13",
"ipython==9.12.0",
"pyfakefs==6.2.0",
"ipython==9.10.0",
"pyfakefs==6.1.3",
"pylint-django==2.7.0",
"pylint<4.0.0",
"pytest-cov==7.1.0",
"pytest-cov==7.0.0",
"pytest-django==4.12.0",
"pytest==9.0.3",
"pytest==9.0.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.26.0",
"ruff==0.15.11",
"types-requests==2.33.0.20260408",
"ruff==0.15.4",
"types-requests==2.32.4.20260107",
]
[tool.setuptools]

View File

@@ -60,7 +60,7 @@ COPY --from=impress-builder /home/frontend/apps/impress/out /app
FROM ${FRONTEND_IMAGE} AS frontend-source
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.23 AS frontend-production
FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production
# Upgrade system packages to install security updates
USER root

View File

@@ -66,8 +66,6 @@ if (process.env.IS_INSTANCE !== 'true') {
name: 'Albert AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
});
await mockAIResponse(page);
@@ -133,8 +131,6 @@ if (process.env.IS_INSTANCE !== 'true') {
name: 'Albert AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
});
await mockAIResponse(page);
@@ -170,11 +166,6 @@ if (process.env.IS_INSTANCE !== 'true') {
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
});
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
@@ -238,11 +229,6 @@ if (process.env.IS_INSTANCE !== 'true') {
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
});
await mockedDocument(page, {
accesses: [
{
@@ -317,11 +303,6 @@ if (process.env.IS_INSTANCE !== 'true') {
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
});
await mockedDocument(page, {
accesses: [
{

View File

@@ -65,6 +65,19 @@ test.describe('Doc Editor', () => {
toolbar.locator('button[data-test="createLink"]'),
).toBeVisible();
/**
* Because of how Posthog is loaded and how auth session are
* saved, this assertion is not reliable on test instances
* We will dedicate a testcase to check the AI features
* on test instances with a specific setup
*/
if (process.env.IS_INSTANCE !== 'true') {
// eslint-disable-next-line playwright/no-conditional-expect
await expect(
toolbar.getByRole('button', { name: 'Ask AI' }),
).toBeVisible();
}
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeVisible();
@@ -96,6 +109,7 @@ test.describe('Doc Editor', () => {
await image.click();
await expect(toolbar.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeHidden();

View File

@@ -144,36 +144,6 @@ test.describe('Doc Header', () => {
await cleanup();
});
test('it pastes plain text in the title without keeping formatting', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-title-paste', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await docTitle.click();
await page.keyboard.press('Control+a');
await page.evaluate(() => {
const el = document.querySelector('[aria-label="Document title"]');
if (!el) {
return;
}
const dt = new DataTransfer();
dt.setData('text/plain', 'Pasted plain text');
dt.setData('text/html', '<b><em>Pasted plain text</em></b>');
el.dispatchEvent(
new ClipboardEvent('paste', { clipboardData: dt, bubbles: true }),
);
});
await docTitle.blur();
await expect(docTitle).toHaveText('Pasted plain text');
// Ensure formatting tags from text/html were not inserted.
await expect(docTitle.locator('b, em, strong, i')).toHaveCount(0);
});
test('it updates the title doc adding a leading emoji', async ({
page,
browserName,
@@ -531,7 +501,7 @@ test.describe('Doc Header', () => {
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
);
const uuid = await mockedDocument(page, {
await mockedDocument(page, {
abilities: {
destroy: false, // Means owner
link_configuration: true,
@@ -564,7 +534,9 @@ test.describe('Doc Header', () => {
const clipboardContent = await handle.jsonValue();
const origin = await page.evaluate(() => window.location.origin);
expect(clipboardContent.trim()).toMatch(`${origin}/docs/${uuid}/`);
expect(clipboardContent.trim()).toMatch(
`${origin}/docs/mocked-document-id/`,
);
});
test('it pins a document', async ({ page, browserName }) => {

View File

@@ -131,7 +131,7 @@ test.describe('Language', () => {
await waitForLanguageSwitch(page, TestLanguage.French);
// Check for French 404 response
await check404Response('Non trouvé.');
await check404Response('Pas trouvé.');
});
test('it check translations of the slash menu when changing language', async ({

View File

@@ -13,8 +13,8 @@ export const CONFIG = {
name: 'Docs AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: false,
AI_FEATURE_BLOCKNOTE_ENABLED: false,
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
API_USERS_SEARCH_QUERY_MIN_LENGTH: 3,
CRISP_WEBSITE_ID: null,
@@ -137,10 +137,13 @@ export const createDoc = async (
})
.click();
const input = page.getByLabel('Document title');
await expect(input).toBeVisible({
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'networkidle',
});
const input = page.getByLabel('Document title');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await input.fill(randomDocs[i]);
@@ -248,7 +251,6 @@ export const waitForResponseCreateDoc = (page: Page) => {
export const mockedDocument = async (page: Page, data: object) => {
// document/[ID]/ or document/[ID]/tree/ routes
const uuid = crypto.randomUUID();
await page.route(/.*\/documents\/[^/]+\/(?:$|tree\/.*)/, async (route) => {
const request = route.request();
if (request.method().includes('GET') && !request.url().includes('page=')) {
@@ -257,7 +259,7 @@ export const mockedDocument = async (page: Page, data: object) => {
};
await route.fulfill({
json: {
id: uuid,
id: 'mocked-document-id',
title: 'Mocked document',
path: '000000',
abilities: {
@@ -302,8 +304,6 @@ export const mockedDocument = async (page: Page, data: object) => {
await route.continue();
}
});
return uuid;
};
export const mockedListDocs = async (page: Page, data: object[] = []) => {

View File

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

View File

@@ -6,7 +6,6 @@ const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
/** @type {import('next').NextConfig} */
const nextConfig = {
allowedDevOrigins: ['docs.127.0.0.1.nip.io'],
output: 'export',
trailingSlash: true,
images: {

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "5.0.0",
"version": "4.8.6",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -76,7 +76,7 @@
"react-select": "5.10.2",
"styled-components": "6.3.12",
"use-debounce": "10.1.0",
"uuid": "14.0.0",
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
"zod": "4.3.6",

View File

@@ -153,40 +153,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
}
};
const insertPlainText = (plainText: string, target: HTMLElement) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
if (!target.contains(range.commonAncestorContainer)) {
target.focus();
range.selectNodeContents(target);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
range.deleteContents();
range.insertNode(document.createTextNode(plainText));
};
const handlePaste = (event: React.ClipboardEvent<HTMLSpanElement>) => {
event.preventDefault();
insertPlainText(
event.clipboardData.getData('text/plain'),
event.currentTarget,
);
};
const handleDrop = (event: React.DragEvent<HTMLSpanElement>) => {
event.preventDefault();
insertPlainText(
event.dataTransfer.getData('text/plain'),
event.currentTarget,
);
};
useEffect(() => {
setTitleDisplay(isTopRoot ? doc.title : titleWithoutEmoji);
}, [doc.title, isTopRoot, titleWithoutEmoji]);
@@ -215,8 +181,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}
onPasteCapture={handlePaste}
onDropCapture={handleDrop}
$padding={{ right: 'big' }}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ImperativePanelHandle,
Panel,
@@ -16,27 +15,6 @@ const pxToPercent = (px: number) => {
return (px / window.innerWidth) * 100;
};
const RESIZE_HANDLE_ID = 'left-panel-resize-handle';
const getValueLabel = (
current: number,
min: number,
max: number,
t: (key: string) => string,
): string => {
if (max <= min) {
return t('Sidebar width: medium');
}
const ratio = (current - min) / (max - min);
if (ratio < 1 / 3) {
return t('Sidebar width: narrow');
}
if (ratio < 2 / 3) {
return t('Sidebar width: medium');
}
return t('Sidebar width: wide');
};
type ResizableLeftPanelProps = {
leftPanel: React.ReactNode;
children: React.ReactNode;
@@ -50,7 +28,6 @@ export const ResizableLeftPanel = ({
minPanelSizePx = 300,
maxPanelSizePx = 450,
}: ResizableLeftPanelProps) => {
const { t } = useTranslation();
const { isDesktop } = useResponsiveStore();
const { isPanelOpen } = useLeftPanelStore();
const ref = useRef<ImperativePanelHandle>(null);
@@ -119,24 +96,6 @@ export const ResizableLeftPanel = ({
};
}, [isDesktop]);
/**
* Workaround: NVDA does not enter focus mode for role="separator"
* (https://github.com/nvaccess/nvda/issues/11403), so arrow keys are
* intercepted by browse-mode navigation and never reach the handle.
* Changing the role to "slider" makes NVDA reliably switch to focus
* mode, restoring progressive keyboard resize with arrow keys.
*
* Note: PanelResizeHandle does not expose a ref (no RefAttributes in its
* type definition), so we use id + getElementById as the only viable option.
* Only role needs to be overridden here; aria-* props are passed directly.
*/
useEffect(() => {
if (!isPanelOpen) {
return;
}
document.getElementById(RESIZE_HANDLE_ID)?.setAttribute('role', 'slider');
}, [isPanelOpen]);
const handleResize = (sizePercent: number) => {
const widthPx = (sizePercent / 100) * window.innerWidth;
savedWidthPxRef.current = widthPx;
@@ -144,7 +103,7 @@ export const ResizableLeftPanel = ({
};
return (
<PanelGroup direction="horizontal" keyboardResizeBy={1}>
<PanelGroup direction="horizontal">
<Panel
ref={ref}
className="--docs--resizable-left-panel"
@@ -173,18 +132,6 @@ export const ResizableLeftPanel = ({
</Panel>
{isPanelOpen && (
<PanelResizeHandle
id={RESIZE_HANDLE_ID}
aria-label={t('Resize sidebar')}
aria-orientation="horizontal"
aria-valuemin={Math.round(minPanelSizePercent)}
aria-valuemax={Math.round(maxPanelSizePercent)}
aria-valuenow={Math.round(panelSizePercent)}
aria-valuetext={getValueLabel(
panelSizePercent,
minPanelSizePercent,
maxPanelSizePercent,
t,
)}
style={{
borderRightWidth: '1px',
borderRightStyle: 'solid',

View File

@@ -629,7 +629,6 @@
"Change role for {{name}}": "Αλλαγή ρόλου για {{name}}",
"Checklist applied": "Εφαρμόστηκε λίστα ελέγχου",
"Choose a user": "Επιλέξτε ένα χρήστη",
"Choose the email": "Επιλέξτε το email",
"Choose the new location for <strong>{{title}}</strong>.": "Επιλέξτε τη νέα θέση για το <strong>{{title}}</strong>.",
"Close the access request modal": "Κλείσιμο παραθύρου αίτησης πρόσβασης",
"Close the delete modal": "Κλείσιμο παραθύρου διαγραφής",
@@ -687,15 +686,12 @@
"Document tree": "Δομή εγγράφου",
"Document unpinned successfully!": "Το έγγραφο ξεκαρφιτσώθηκε επιτυχώς!",
"Document visibility": "Ορατότητα εγγράφου",
"Document {{activeId}} is over document {{overId}}.": "Το έγγραφο {{activeId}} είναι πάνω από το έγγραφο {{overId}}.",
"Document {{id}} was dropped.": "Το έγγραφο {{id}} απορρίφθηκε.",
"Documents grid": "Πλέγμα εγγράφων",
"Docx": "Docx",
"Download": "Λήψη",
"Download anyway": "Λήψη οπωσδήποτε",
"Download {{format}}": "Λήψη σε {{format}}",
"Drag and drop status": "Κατάσταση μεταφοράς και απόθεσης",
"Dragging was cancelled. Document {{id}} was dropped.": "Ακυρώθηκε η απόσυρση. Το έγγραφο {{id}} απορρίφθηκε.",
"Draw inspiration from the content library": "Αντλήστε έμπνευση από τη βιβλιοθήκη περιεχομένου",
"Duplicate": "Δημιουργία αντιγράφου",
"Edit document emoji": "Επεξεργασία emoji εγγράφου",
@@ -790,7 +786,6 @@
"New sub-doc": "Νέο υπο-έγγραφο",
"No document found": "Δεν βρέθηκε έγγραφο",
"No documents found": "Δεν βρέθηκαν έγγραφα",
"No results. Type a full email address to invite someone.": "Δεν υπάρχουν αποτελέσματα. Πληκτρολογήστε μια πλήρη διεύθυνση email για να προσκαλέσετε κάποιον.",
"No text selected": "Δεν επιλέχθηκε κείμενο",
"No versions": "Δεν υπάρχουν εκδόσεις",
"Numbered list applied": "Εφαρμόστηκε αριθμημένη λίστα",
@@ -818,7 +813,6 @@
"Paragraph applied": "Εφαρμόστηκε παράγραφος",
"Pending invitations": "Εκκρεμείς προσκλήσεις",
"People with access via the parent document": "Άτομα με πρόσβαση μέσω του γονικού εγγράφου",
"Picked up document {{id}}.": "Επιλέχθηκε το έγγραφο {{id}}.",
"Pin": "Καρφίτσωμα",
"Pinned documents": "Καρφιτσωμένα έγγραφα",
"Please download it only if it comes from a trusted source.": "Παρακαλούμε πραγματοποιήστε λήψη μόνο εάν προέρχεται από αξιόπιστη πηγή.",
@@ -879,7 +873,6 @@
"Summarize": "Σύνοψη",
"Summary": "Περίληψη",
"The antivirus has detected an anomaly in your file.": "Το λογισμικό προστασίας από ιούς εντόπισε μια ανωμαλία στο αρχείο σας.",
"The current document will be replaced, but you'll still find it in the version history.": "Το τρέχον έγγραφο θα αντικατασταθεί, αλλά θα το βρείτε ακόμα στο ιστορικό εκδόσεων.",
"The document \"{{documentName}}\" has been successfully imported": "Το έγγραφο \"{{documentName}}\" εισήχθη επιτυχώς",
"The document \"{{documentName}}\" import has failed": "Η εισαγωγή του εγγράφου \"{{documentName}}\" απέτυχε",
"The document \"{{documentName}}\" import has failed (only .docx and .md files are allowed)": "Η εισαγωγή του εγγράφου \"{{documentName}}\" απέτυχε (επιτρέπονται μόνο αρχεία .docx και .md)",
@@ -902,7 +895,6 @@
"Too many requests. Please wait 60 seconds.": "Πάρα πολλά αιτήματα. Παρακαλούμε περιμένετε 60 δευτερόλεπτα.",
"Trashbin": "Κάδος απορριμμάτων",
"Type a name or email": "Πληκτρολογήστε όνομα ή email",
"Type at least {{minLength}} characters to display user names": "Πληκτρολογήστε τουλάχιστον {{minLength}} χαρακτήρες για την εμφάνιση ονομάτων χρηστών",
"Type the name of a document": "Πληκτρολογήστε το όνομα ενός εγγράφου",
"Unpin": "Ξεκαρφίτσωμα",
"Untitled document": "Έγγραφο χωρίς τίτλο",
@@ -1199,7 +1191,6 @@
"An error occurred...": "Une erreur s'est produite...",
"An uncompromising writing experience.": "Une expérience d'écriture sans compromis.",
"An unexpected error occurred.": "Une erreur inattendue sest produite.",
"An unexpected error occurred. Go grab a coffee or try to refresh the page.": "Une erreur inattendue est survenue. Allez prendre un café ou essayez d'actualiser la page.",
"Analyzing file...": "Analyse du fichier...",
"Anonymous": "Anonyme",
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
@@ -1289,7 +1280,6 @@
"Document {{activeId}} was dropped over document {{overId}}.": "Le document {{activeId}} a été abandonné sur le document {{overId}}.",
"Document {{id}} is no longer over a droppable area.": "Le document {{id}} n'est plus au-dessus d'une zone dépotable.",
"Document {{id}} was dropped.": "Le document {{id}} a été abandonné.",
"Documentation": "Documentation",
"Documents grid": "Grille des documents",
"Docx": "Docx",
"Download": "Télécharger",
@@ -1325,7 +1315,6 @@
"Flexible export.": "Un export flexible.",
"Format": "Format",
"Format your content with the toolbar": "Formatez votre contenu avec la barre d'outils",
"Get Support": "Obtenir de l'aide",
"Go to content": "Voir le contenu",
"Govs ❤️ Open Source.": "Gouvernements ❤️ Open Source.",
"HTML": "HTML",
@@ -1443,7 +1432,6 @@
"Request access modal": "Demande d'accès",
"Reset": "Réinitialiser",
"Reset search filters": "Réinitialiser les filtres de recherche",
"Resize sidebar": "Redimensionner la barre latérale",
"Restore": "Restaurer",
"Restore version of {{date}}": "Restaurer la version de {{date}}",
"Restoring an older version": "Restauration d'une ancienne version en cours",
@@ -1452,7 +1440,6 @@
"Search docs": "Rechercher des docs",
"Search documents": "Rechercher des documents",
"Search for a doc": "Rechercher un document",
"Search for a document": "Rechercher un document",
"Search modal": "Modale de recherche",
"Search results": "Résultats de la recherche",
"Select a document": "Sélectionnez un document",
@@ -1475,9 +1462,6 @@
"Show more": "Voir plus",
"Show the side panel for {{title}}": "Afficher le panneau latéral pour {{title}}",
"Show the table of contents": "Afficher la table des matières",
"Sidebar width: medium": "Largeur de la barre latérale : moyenne",
"Sidebar width: narrow": "Largeur de la barre latérale: étroite",
"Sidebar width: wide": "Largeur de la barre latérale: large",
"Simple and secure collaboration.": "Une collaboration simple et sécurisée.",
"Simple document icon": "Icône simple du document",
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
@@ -1548,7 +1532,6 @@
"home-content-open-source-part2": "Vous pouvez facilement auto-héberger Docs (consultez notre <2>documentation</2> d'installation).<br/>Docs utilise une <7>licence</7> (MIT) adaptée à l'innovation et aux entreprises.<br/>Les contributions sont les bienvenues (consultez notre feuille de route <13>ici</13>).",
"home-content-open-source-part3": "Docs est le résultat d'un effort conjoint mené par les gouvernements français 🇫🇷🥖 <1>(DINUM)</1> et allemand 🇩🇪🥨 <5>(ZenDiS)</5>.",
"just now": "à l'instant",
"mention a sub-doc...": "mentionner un sous-document...",
"new window": "nouvelle fenêtre",
"pdf": "pdf",
"src_img_onboarding_step_1": "/assets/on-boarding/step_1_FR.gif",
@@ -2071,7 +2054,6 @@
"An error occurred...": "Произошла ошибка...",
"An uncompromising writing experience.": "Бескомпромиссный опыт написания.",
"An unexpected error occurred.": "Произошла непредвиденная ошибка.",
"An unexpected error occurred. Go grab a coffee or try to refresh the page.": "Произошла непредвиденная ошибка. Сходите за чашкой кофе или попробуйте обновить страницу.",
"Analyzing file...": "Анализ файла...",
"Anonymous": "Аноним",
"Anyone with the link can edit the document": "Любой, у кого есть ссылка, может редактировать документ",
@@ -2161,7 +2143,6 @@
"Document {{activeId}} was dropped over document {{overId}}.": "Документ {{activeId}} был сброшен поверх документа {{overId}}.",
"Document {{id}} is no longer over a droppable area.": "Документ {{id}} больше не находится над местом, куда его можно поместить.",
"Document {{id}} was dropped.": "Документ {{id}} сброшен.",
"Documentation": "Документация",
"Documents grid": "Сетка документов",
"Docx": "Docx",
"Download": "Загрузить",
@@ -2197,7 +2178,6 @@
"Flexible export.": "Полезные форматы экспорта.",
"Format": "Формат",
"Format your content with the toolbar": "Форматируйте содержимое с помощью панели инструментов",
"Get Support": "Получить поддержку",
"Go to content": "Перейти к содержимому",
"Govs ❤️ Open Source.": "Govs ❤️ Open Source.",
"HTML": "HTML",
@@ -2315,7 +2295,6 @@
"Request access modal": "Запрос доступа",
"Reset": "Сброс",
"Reset search filters": "Сбросить фильтры поиска",
"Resize sidebar": "Изменить размер панели",
"Restore": "Восстановить",
"Restore version of {{date}}": "Восстановить версию от {{date}}",
"Restoring an older version": "Восстановление более старой версии",
@@ -2324,7 +2303,6 @@
"Search docs": "Поиск документов",
"Search documents": "Поиск документов",
"Search for a doc": "Поиск документов",
"Search for a document": "Поиск документа",
"Search modal": "Поиск",
"Search results": "Результаты поиска",
"Select a document": "Выберите документ",
@@ -2347,9 +2325,6 @@
"Show more": "Показать ещё",
"Show the side panel for {{title}}": "Показать боковую панель для {{title}}",
"Show the table of contents": "Показать оглавление",
"Sidebar width: medium": "Ширина боковой панели: средняя",
"Sidebar width: narrow": "Ширина боковой панели: узкая",
"Sidebar width: wide": "Ширина боковой панели: широкая",
"Simple and secure collaboration.": "Простое и безопасное сотрудничество.",
"Simple document icon": "Простой значок документа",
"Something bad happens, please retry.": "Что-то пошло не так, повторите попытку.",
@@ -2420,7 +2395,6 @@
"home-content-open-source-part2": "Вы можете легко разместить Docs у себя (см. нашу <2>документацию по установке</2>).<br/>Docs использует <7>лицензию</7> (MIT), подходящую для инноваций и бизнеса.<br/>Мы приветствуем ваши вклады (см. наш план разработки <13>здесь</13>).",
"home-content-open-source-part3": "Docs — это результат совместных усилий правительств Франции 🇫🇷🥖 <1>(DINUM)</1> и Германии 🇩🇪🥨 <5>(ZenDiS)</5>.",
"just now": "только что",
"mention a sub-doc...": "упоминание вложенного документа...",
"new window": "новое окно",
"pdf": "pdf",
"src_img_onboarding_step_1": "src_img_onboarding_step_1",
@@ -2569,7 +2543,6 @@
"An error occurred...": "Виникла помилка...",
"An uncompromising writing experience.": "Безкомпромісне задоволення від процесу письма.",
"An unexpected error occurred.": "Сталася неочікувана помилка.",
"An unexpected error occurred. Go grab a coffee or try to refresh the page.": "Сталася неочікувана помилка. Сходіть за кавою або спробуйте оновити сторінку.",
"Analyzing file...": "Аналіз файлу...",
"Anonymous": "Анонім",
"Anyone with the link can edit the document": "Будь-хто з посиланням може редагувати документ",
@@ -2659,7 +2632,6 @@
"Document {{activeId}} was dropped over document {{overId}}.": "Документ {{activeId}} відпущений над документом {{overId}}.",
"Document {{id}} is no longer over a droppable area.": "Документ {{id}} більше не в зоні для пересування.",
"Document {{id}} was dropped.": "Документ {{id}} був відхилений.",
"Documentation": "Документація",
"Documents grid": "Сітка документів",
"Docx": "Docx",
"Download": "Завантажити",
@@ -2695,7 +2667,6 @@
"Flexible export.": "Гнучкий експорт.",
"Format": "Формат",
"Format your content with the toolbar": "Форматуйте вміст за допомогою панелі інструментів",
"Get Support": "Отримати підтримку",
"Go to content": "Перейти до вмісту",
"Govs ❤️ Open Source.": "Govs ❤️ Open Source.",
"HTML": "HTML",
@@ -2813,7 +2784,6 @@
"Request access modal": "Запит доступу",
"Reset": "Скинути",
"Reset search filters": "Скинути фільтри пошуку",
"Resize sidebar": "Змінити розмір бічної панелі",
"Restore": "Відновити",
"Restore version of {{date}}": "Відновити версію від {{date}}",
"Restoring an older version": "Відновлення старішої версії",
@@ -2822,7 +2792,6 @@
"Search docs": "Пошук документів",
"Search documents": "Пошук документів",
"Search for a doc": "Пошук документу",
"Search for a document": "Пошук документа",
"Search modal": "Пошук",
"Search results": "Результати пошуку",
"Select a document": "Оберіть документ",
@@ -2845,9 +2814,6 @@
"Show more": "Показати більше",
"Show the side panel for {{title}}": "Показувати бічну панель для {{title}}",
"Show the table of contents": "Показати зміст",
"Sidebar width: medium": "Ширина бічної панелі: середня",
"Sidebar width: narrow": "Ширина бічної панелі: вузька",
"Sidebar width: wide": "Ширина бічної панелі: широка",
"Simple and secure collaboration.": "Проста та безпечна співпраця.",
"Simple document icon": "Проста піктограма документа",
"Something bad happens, please retry.": "Сталася помилка, спробуйте ще раз.",
@@ -2918,7 +2884,6 @@
"home-content-open-source-part2": "Ви можете легко самостійно розмістити Docs (див. нашу <2>документацію з встановлення</2>).<br/>Docs використовує <7>ліцензію</7> (MIT), що підходить для інновацій та бізнесу.<br/>Внески вітаються (див. наш план розробки <13>тут</13>).",
"home-content-open-source-part3": "Docs є результатом спільних зусиль, очолюваних урядами Франції 🇫🇷🥖 <1>(DINUM)</1> та Німеччини 🇩🇪🥨 <5>(ZenDiS)</5>.",
"just now": "щойно",
"mention a sub-doc...": "згадка про вкладений документ...",
"new window": "нове вікно",
"pdf": "pdf",
"src_img_onboarding_step_1": "src_img_onboarding_step_1",

View File

@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "5.0.0",
"version": "4.8.6",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -36,12 +36,12 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"eslint": "10.1.0",
"postcss": "8.5.10",
"glob": "13.0.6",
"prosemirror-view": "1.41.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"serialize-javascript": "7.0.5",
"typescript": "5.9.3",
"uuid": "14.0.0",
"wrap-ansi": "10.0.0",
"yjs": "13.6.30"
},

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "5.0.0",
"version": "4.8.6",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -25,7 +25,7 @@
"cors": "2.8.6",
"express": "5.2.1",
"express-ws": "5.0.2",
"uuid": "14.0.0",
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*"
},

View File

@@ -18,3 +18,6 @@ export const PORT = Number(process.env.PORT || 4444);
export const SENTRY_DSN = process.env.SENTRY_DSN || '';
export const COLLABORATION_BACKEND_BASE_URL =
process.env.COLLABORATION_BACKEND_BASE_URL || 'http://app-dev:8000';
export const COLLABORATION_INACTIVITY_TIMEOUT = Number(
process.env.COLLABORATION_INACTIVITY_TIMEOUT || 0,
);

View File

@@ -1,9 +1,14 @@
import { Request } from 'express';
import * as ws from 'ws';
import { COLLABORATION_INACTIVITY_TIMEOUT } from '@/env';
import { hocuspocusServer } from '@/servers/hocuspocusServer';
import { setupInactivityTimeout } from '@/utils';
export const collaborationWSHandler = (ws: ws.WebSocket, req: Request) => {
if (COLLABORATION_INACTIVITY_TIMEOUT > 0) {
setupInactivityTimeout(ws, COLLABORATION_INACTIVITY_TIMEOUT);
}
try {
hocuspocusServer.hocuspocus.handleConnection(ws, req);
} catch (error) {

View File

@@ -1,3 +1,5 @@
import * as ws from 'ws';
import { COLLABORATION_LOGGING } from './env';
export function logger(...args: unknown[]) {
@@ -9,3 +11,25 @@ export function logger(...args: unknown[]) {
export const toBase64 = function (str: Uint8Array) {
return Buffer.from(str).toString('base64');
};
export function setupInactivityTimeout(
socket: ws.WebSocket,
delayMs: number,
): void {
const closeInactive = () => {
logger('Closing inactive WebSocket connection after', delayMs, 'ms');
socket.close();
};
let timer = setTimeout(closeInactive, delayMs);
socket.on('message', () => {
logger('clear closeInactive timer');
clearTimeout(timer);
timer = setTimeout(closeInactive, delayMs);
});
socket.on('close', () => {
clearTimeout(timer);
});
}

View File

@@ -2302,18 +2302,6 @@
dependencies:
"@swc/helpers" "^0.5.0"
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
dependencies:
string-width "^5.1.2"
string-width-cjs "npm:string-width@^4.2.0"
strip-ansi "^7.0.1"
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -3302,11 +3290,6 @@
"@noble/hashes" "^2.0.1"
error-causes "^3.0.2"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@pkgr/core@^0.2.9":
version "0.2.9"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b"
@@ -6169,87 +6152,87 @@
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
"@rolldown/binding-android-arm64@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz#4e6af08b89da02596cc5da4b105082b68673ffec"
integrity sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==
"@rolldown/binding-android-arm64@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz#25a584227ed97239fd564451c0db2c359751b42a"
integrity sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==
"@rolldown/binding-darwin-arm64@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz#a06890f4c9b48ff0fc97edbedfc762bef7cffd73"
integrity sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==
"@rolldown/binding-darwin-arm64@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz#dcfa96c4d8c7baa47f5b90294ce8ebf1b0b1dbf9"
integrity sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==
"@rolldown/binding-darwin-x64@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz#eddf6aa3ed3509171fe21711f1e8ec8e0fd7ec49"
integrity sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==
"@rolldown/binding-darwin-x64@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz#6e751ea2067cacee0c94f0e8b087761dde62f9ea"
integrity sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==
"@rolldown/binding-freebsd-x64@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz#2102dfed19fd1f1b53435fcaaf0bc61129a266a3"
integrity sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==
"@rolldown/binding-freebsd-x64@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz#b7582b959398c5871034b94ba0a8ecde0425a8e7"
integrity sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==
"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz#b2c13f40e990fd1e1935492850536c768c961a0f"
integrity sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==
"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz#3b8c5e071d6a0ed1cb1880c1948c6fece553502a"
integrity sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==
"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz#32ca9f77c1e76b2913b3d53d2029dc171c0532d6"
integrity sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==
"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz#2533165620137b077ae4ede92b752a63cd85cfcb"
integrity sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==
"@rolldown/binding-linux-arm64-musl@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz#f4337ddd52f0ed3ada2105b59ee1b757a2c4858c"
integrity sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==
"@rolldown/binding-linux-arm64-musl@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz#b04cf5b806a012027a4e8b139e0f86b2ff7621c0"
integrity sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==
"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz#22fdd14cb00ee8208c28a39bab7f28860ec6705d"
integrity sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==
"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz#bda9c11fe03482033d5dac6a943802b3e7579550"
integrity sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==
"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz#838215096d1de6d3d509e0410801cb7cda8161ff"
integrity sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==
"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz#55daa2d35f92f62e958fc44e12db1c16e1f271c5"
integrity sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==
"@rolldown/binding-linux-x64-gnu@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz#f7d71d97f6bd43198596b26dc2cb364586e12673"
integrity sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==
"@rolldown/binding-linux-x64-gnu@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz#8ca1abf607bbe2f7fdd6f6416192937dc9ea1e54"
integrity sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==
"@rolldown/binding-linux-x64-musl@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz#a2ca737f01b0ad620c4c404ca176ea3e3ad804c3"
integrity sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==
"@rolldown/binding-linux-x64-musl@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz#36a52beee8ac97a79d1ed8f1b94fab677e3e4d11"
integrity sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==
"@rolldown/binding-openharmony-arm64@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz#f66317e29eafcc300bed7af8dddac26ab3b1bf82"
integrity sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==
"@rolldown/binding-openharmony-arm64@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz#91c74fd23b3f3f3942fe4b3aefc9428ecbaa55fd"
integrity sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==
"@rolldown/binding-wasm32-wasi@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz#8825523fdffa1f1dc4683be9650ffaa9e4a77f04"
integrity sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==
"@rolldown/binding-wasm32-wasi@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz#6520bafe57ff1cd2fb45f8f22b1cb6d57be44e79"
integrity sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==
dependencies:
"@napi-rs/wasm-runtime" "^1.1.1"
"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz#4f3a17e3d68a58309c27c0930b0f7986ccabef47"
integrity sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==
"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz#73dd1c4737473c8270b61cd2e42b05a34453ffc0"
integrity sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==
"@rolldown/binding-win32-x64-msvc@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz#d762765d5660598a96b570b513f535c151272985"
integrity sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==
"@rolldown/binding-win32-x64-msvc@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz#4d922aa6dd6bf27c73eba93fec9a0aed62549095"
integrity sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==
"@rolldown/pluginutils@1.0.0-rc.12":
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz#74163aec62fa51cee18d62709483963dceb3f6dc"
integrity sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==
"@rolldown/pluginutils@1.0.0-rc.11":
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz#110d8cc72990c4e36a79791eeafe7cca979e00c9"
integrity sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==
"@rolldown/pluginutils@1.0.0-rc.7":
version "1.0.0-rc.7"
@@ -9758,9 +9741,9 @@ domhandler@^5.0.2, domhandler@^5.0.3:
domelementtype "^2.3.0"
dompurify@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.0.tgz#b1fc33ebdadb373241621e0a30e4ad81573dfd0b"
integrity sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==
version "3.3.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@@ -9811,11 +9794,6 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -10751,9 +10729,9 @@ flatted@^3.2.9, flatted@^3.3.3:
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
follow-redirects@^1.15.11:
version "1.16.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
version "1.15.11"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
fontkit@^2.0.2:
version "2.0.4"
@@ -10777,14 +10755,6 @@ for-each@^0.3.3, for-each@^0.3.5:
dependencies:
is-callable "^1.2.7"
foreground-child@^3.1.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
dependencies:
cross-spawn "^7.0.6"
signal-exit "^4.0.1"
form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
@@ -10889,11 +10859,6 @@ fs-tree-diff@^2.0.1:
path-posix "^1.0.0"
symlink-or-copy "^1.1.8"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
@@ -11051,19 +11016,7 @@ glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^10.5.0:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
minimatch "^9.0.4"
minipass "^7.1.2"
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^13.0.6:
glob@13.0.6, glob@^10.5.0, glob@^13.0.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "13.0.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d"
integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==
@@ -11072,18 +11025,6 @@ glob@^13.0.6:
minipass "^7.1.3"
path-scurry "^2.0.2"
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-modules@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -11710,15 +11651,7 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -12110,15 +12043,6 @@ iterator.prototype@^1.1.4:
has-symbols "^1.1.0"
set-function-name "^2.0.2"
jackspeak@^3.1.2:
version "3.4.3"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jake@^10.8.5:
version "10.9.4"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6"
@@ -12984,7 +12908,7 @@ lower-case@^2.0.2:
dependencies:
tslib "^2.0.3"
lru-cache@^10.2.0, lru-cache@^10.4.3:
lru-cache@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
@@ -13631,7 +13555,7 @@ minimatch@^10.2.1, minimatch@^10.2.2, minimatch@^10.2.4, "minimatch@^9.0.3 || ^1
dependencies:
brace-expansion "^5.0.2"
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.2:
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
@@ -13645,7 +13569,7 @@ minimatch@^5.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4, minimatch@^9.0.5:
minimatch@^9.0.5:
version "9.0.9"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
@@ -13657,16 +13581,16 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
minipass@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
mktemp@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
@@ -13692,7 +13616,7 @@ mylas@^2.1.9:
resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4"
integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==
nanoid@^3.3.11:
nanoid@^3.3.11, nanoid@^3.3.6, nanoid@^3.3.7:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
@@ -13913,7 +13837,7 @@ on-finished@^2.4.1:
dependencies:
ee-first "1.1.1"
once@^1.3.0, once@^1.4.0:
once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
@@ -14000,11 +13924,6 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
package-json-from-dist@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
pako@^0.2.5:
version "0.2.9"
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@@ -14076,11 +13995,6 @@ path-exists@^4.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -14096,14 +14010,6 @@ path-posix@^1.0.0:
resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f"
integrity sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==
path-scurry@^1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
dependencies:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-scurry@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85"
@@ -14183,7 +14089,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4:
picomatch@^4.0.2, picomatch@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
@@ -14261,10 +14167,37 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.31, postcss@8.4.49, postcss@8.5.10, postcss@^8.5.6, postcss@^8.5.8:
version "8.5.10"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.10.tgz#8992d8c30acf3f12169e7c09514a12fed7e48356"
integrity sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@8.4.49:
version "8.4.49"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
dependencies:
nanoid "^3.3.7"
picocolors "^1.1.1"
source-map-js "^1.2.1"
postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
postcss@^8.5.8:
version "8.5.8"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399"
integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
@@ -14579,9 +14512,9 @@ prosemirror-view@1.41.7, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prose
prosemirror-transform "^1.1.0"
protobufjs@^7.3.0:
version "7.5.5"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.5.tgz#b7089ca4410374c75150baf277353ef76db69f96"
integrity sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==
version "7.5.4"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a"
integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
@@ -14691,13 +14624,6 @@ raf@^3.4.1:
dependencies:
performance-now "^2.1.0"
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
dependencies:
safe-buffer "^5.1.0"
range-parser@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -15531,29 +15457,29 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rolldown@1.0.0-rc.12:
version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.12.tgz#e226fa74a4c21c71a13f8e44f778f81d58853ad5"
integrity sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==
rolldown@1.0.0-rc.11:
version "1.0.0-rc.11"
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.11.tgz#6eaf091b1bbb5ed92e5302171a3d59f0d026d9c0"
integrity sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==
dependencies:
"@oxc-project/types" "=0.122.0"
"@rolldown/pluginutils" "1.0.0-rc.12"
"@rolldown/pluginutils" "1.0.0-rc.11"
optionalDependencies:
"@rolldown/binding-android-arm64" "1.0.0-rc.12"
"@rolldown/binding-darwin-arm64" "1.0.0-rc.12"
"@rolldown/binding-darwin-x64" "1.0.0-rc.12"
"@rolldown/binding-freebsd-x64" "1.0.0-rc.12"
"@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.12"
"@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.12"
"@rolldown/binding-linux-arm64-musl" "1.0.0-rc.12"
"@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.12"
"@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.12"
"@rolldown/binding-linux-x64-gnu" "1.0.0-rc.12"
"@rolldown/binding-linux-x64-musl" "1.0.0-rc.12"
"@rolldown/binding-openharmony-arm64" "1.0.0-rc.12"
"@rolldown/binding-wasm32-wasi" "1.0.0-rc.12"
"@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.12"
"@rolldown/binding-win32-x64-msvc" "1.0.0-rc.12"
"@rolldown/binding-android-arm64" "1.0.0-rc.11"
"@rolldown/binding-darwin-arm64" "1.0.0-rc.11"
"@rolldown/binding-darwin-x64" "1.0.0-rc.11"
"@rolldown/binding-freebsd-x64" "1.0.0-rc.11"
"@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.11"
"@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.11"
"@rolldown/binding-linux-arm64-musl" "1.0.0-rc.11"
"@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.11"
"@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.11"
"@rolldown/binding-linux-x64-gnu" "1.0.0-rc.11"
"@rolldown/binding-linux-x64-musl" "1.0.0-rc.11"
"@rolldown/binding-openharmony-arm64" "1.0.0-rc.11"
"@rolldown/binding-wasm32-wasi" "1.0.0-rc.11"
"@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.11"
"@rolldown/binding-win32-x64-msvc" "1.0.0-rc.11"
rollup@^2.43.1:
version "2.80.0"
@@ -15650,7 +15576,7 @@ safe-array-concat@^1.1.3:
has-symbols "^1.1.0"
isarray "^2.0.5"
safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
safe-buffer@5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -15741,12 +15667,10 @@ send@^1.1.0, send@^1.2.0:
range-parser "^1.2.1"
statuses "^2.0.1"
serialize-javascript@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
dependencies:
randombytes "^2.1.0"
serialize-javascript@7.0.5, serialize-javascript@^6.0.1:
version "7.0.5"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1"
integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==
serve-static@^2.2.0:
version "2.2.0"
@@ -15958,7 +15882,7 @@ source-list-map@^2.0.0:
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
source-map-js@^1.0.1, source-map-js@^1.2.1:
source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -16097,7 +16021,7 @@ string-length@^4.0.2:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -16106,24 +16030,6 @@ string-length@^4.0.2:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
dependencies:
eastasianwidth "^0.2.0"
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string-width@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc"
@@ -16240,13 +16146,6 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -16254,13 +16153,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.2:
version "7.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3"
integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==
dependencies:
ansi-regex "^6.2.2"
strip-ansi@^7.1.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"
@@ -16268,6 +16160,13 @@ strip-ansi@^7.1.0:
dependencies:
ansi-regex "^6.0.1"
strip-ansi@^7.1.2:
version "7.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3"
integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==
dependencies:
ansi-regex "^6.2.2"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -17190,10 +17089,20 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@14.0.0, uuid@^8.3.2, uuid@^9.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d"
integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==
uuid@13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8"
integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
@@ -17302,15 +17211,15 @@ vite-compatible-readable-stream@^3.6.1:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
vite@8.0.5, "vite@^6.0.0 || ^7.0.0 || ^8.0.0-0":
version "8.0.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.5.tgz#5f8648997359e18dbc1a9e151ce55434ce5d8a2f"
integrity sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==
"vite@^6.0.0 || ^7.0.0 || ^8.0.0-0":
version "8.0.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.2.tgz#fcee428eb0ad3d4aa9843d7f7ba981679bbe5edc"
integrity sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==
dependencies:
lightningcss "^1.32.0"
picomatch "^4.0.4"
picomatch "^4.0.3"
postcss "^8.5.8"
rolldown "1.0.0-rc.12"
rolldown "1.0.0-rc.11"
tinyglobby "^0.2.15"
optionalDependencies:
fsevents "~2.3.3"
@@ -17785,16 +17694,7 @@ workbox-window@7.1.0:
"@types/trusted-types" "^2.0.2"
workbox-core "7.1.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@10.0.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0, wrap-ansi@^9.0.0:
wrap-ansi@10.0.0, wrap-ansi@^7.0.0, wrap-ansi@^9.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz#b83ddcc14dbc5596f1b07e153bf6f863c1acbb57"
integrity sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==

View File

@@ -120,6 +120,10 @@ backend:
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
themeCustomization:
enabled: true
file_content: {{ readFile "./configuration/theme/demo.json" }}
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs

View File

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

View File

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

View File

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

View File

@@ -538,9 +538,9 @@ leac@^0.6.0:
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
lodash@^4.17.21:
version "4.18.0"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.0.tgz#dfd726f07ab2e39dd763de28fcf66e395c03e440"
integrity sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA==
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
lower-case@^1.1.1:
version "1.1.4"
@@ -998,10 +998,10 @@ peberminta@^0.9.0:
resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352"
integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==
picomatch@2.3.2, picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
proto-list@~1.2.1:
version "1.2.4"