Compare commits

..

43 Commits

Author SHA1 Message Date
Cyril
1fb0897ebe (frontend) fix tree keyboard toggle when children not yet loaded
node expansion now triggers lazy loading even without prior mouse interaction

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 13:17:34 +02:00
Cyril
69e7235f75 (frontend) refine focus outline with shadow for visual consistency
aligns focus state with app style by adding background shadow to outline

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 10:56:34 +02:00
Cyril
942c90c29f (frontend) enable enter key to open documents and subdocuments
added keyboard support to open docs and subdocs using the enter key

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 10:26:49 +02:00
virgile-dev
c5f0142671 📝 (doc) add mosa.cloud docs instance (#1334)
## Purpose

So that users have more options to choose from


## Proposal
Add mosa.cloud docs instance url

Please ensure the following items are checked before submitting your
pull request:
- [x] I have read and followed the [contributing
guidelines](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md)
- [x] I have read and agreed to the [Code of
Conduct](https://github.com/suitenumerique/docs/blob/main/CODE_OF_CONDUCT.md)
- [x] I have signed off my commits with `git commit --signoff` (DCO
compliance)
- [x] I have signed my commits with my SSH or GPG key (`git commit -S`)
- [x] My commit messages follow the required format: `<gitmoji>(type)
title description`
- [ ] I have added a changelog entry under `## [Unreleased]` section (if
noticeable change)
- [ ] I have added corresponding tests for new features or bug fixes (if
applicable)

Signed-off-by: virgile-deville <virgile.deville@beta.gouv.fr>
2025-09-16 07:01:10 +00:00
Manuel Raynaud
7f37d3bda4 🐛(backend) duplicate sub docs as root for reader user
Reader user should be able to duplicate a doc in the doc tree. It should
be created a new doc at the root level.
2025-09-15 20:44:58 +00:00
Manuel Raynaud
7033d0ecf7 🐛(backend) cast DOCUMENT_IMAGE_MAX_SIZE in integer
The expected type for the settings DOCUMENT_IMAGE_MAX_SIZE is an
integer. By not using django configurations IntegerValue, the value is
used as it and most of the time will be a string. We must use the
IntegerValue in order to cast the value in string.
2025-09-15 17:47:43 +02:00
Fabre Florian
0dd6818e91 (frontend) Adapt e2e test utils to the Keycloak 26.3 login page
Fix the keyCloakSignIn() function for the new login page.

Signed-off-by: Fabre Florian <ffabre@hybird.org>
2025-09-15 11:19:42 +02:00
Fabre Florian
eb225fc86f 🔧(keycloak) Fix https required issue in dev mode
On some environments keycloak returns a 'HTTPS required' message on login.
The same issue was fixed in drive by changing the 'sslRequired' value
from 'external' to 'none'.
Also upgrade keycloak up to 26.3.2

Signed-off-by: Fabre Florian <ffabre@hybird.org>
2025-09-15 11:19:41 +02:00
Anthony LC
b893a29138 🔖(minor) release 3.7.0
Added:
- (api) add API route to fetch document content

Changed:
- 🔒️(backend) configure throttle on every viewsets
- ⬆️ Bump eslint to V9
- (frontend) improve accessibility:
  - fix major accessibility issues reported
  by wave and axe
  - unify tab focus style for better visual consistency
  - improve modal a11y: structure, labels, and title
  - improve accessibility of cdoc content with
  correct aria tags
  - unify tab focus style for better visual consistency
  - hide decorative icons, label menus, avoid
  accessible name
- ♻️(tilt) use helm dev-backend chart

Removed:
- 🔥(frontend) remove multi column drop cursor

Fixed:
- 🐛(frontend) fix callout emoji list
2025-09-12 14:21:13 +02:00
Anthony LC
a812580d6c ♻️(frontend) add categories on top of the EmojiPicker
In a recent fix we had to remove the categories
from the EmojiPicker component due to a bug in the
underlying library. This commit reintroduces the
categories feature, placing them at the top of the
picker for improved user experience. The
categories help users quickly find emojis
by grouping them into relevant sections.

We set the default color as well to ensure
consistency across the emoji picker.
2025-09-12 14:21:13 +02:00
AntoLC
1062e38c92 🌐(i18n) update translated strings
Update translated files with new translations
2025-09-12 12:11:02 +02:00
renovate[bot]
62e122b05f ⬆️(dependencies) update js dependencies 2025-09-12 11:33:17 +02:00
Anthony LC
32bc2890e0 📌(dependencies) pin wrap-ansi to 9.0.2
By security we pin wrap-ansi to 9.0.2,
the 9.0.1 version being infected.
2025-09-12 10:32:40 +02:00
Anthony LC
3c3686dc7e 🔧(frontend) add meta information to package.json files
- Add missing repository, author, and license fields
- Add recommended packageManager
2025-09-12 10:20:01 +02:00
Anthony LC
ab90611c36 🔥(frontend) remove multi column drop cursor
The drop cursor for multi column was causing
issues with the editor's usability.
This commit removes the custom drop cursor
implementation to enhance user experience.
2025-09-11 16:11:48 +02:00
Cyril
f9c08cf5ec Revert "(frontend) add document visible in list and openable via enter key"
This reverts commit b619850b1420421f09f56aa8644a93e0fa698682.
2025-09-11 13:43:36 +02:00
Cyril
2155c2ff1f (frontend) add document visible in list and openable via enter key
the document now appears in the list and can be opened using the enter key

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-11 13:43:35 +02:00
Cyril
ef08ba3a00 (frontend) hide decorative icons, label menus, avoid name duplicates
improves a11y by hiding decorative icons, labeling menus and deduping names

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-11 13:43:20 +02:00
Anthony LC
7a903041f8 🐛(frontend) fix callout emoji list
Removing explicit categories prop from
EmojiPicker to let emoji-mart manage categories
and avoid mismatch causing runtime error with
locales.
2025-09-11 11:54:52 +02:00
Anthony LC
4f2e07f949 🛂(frontend) limit input search to 254 characters
254 characters should be sufficient for most
of our usecases.
Limit input search to 254 characters to prevent
errors caused by overly long email addresses.
2025-09-10 16:11:16 +02:00
Anthony LC
8c1e95c587 (demo) change email from user to user.test in demo
When we create a new user in the demo environment,
the email address will now follow the format
user.test@example.com instead of user@example.com.
"user" was only 4 characters long, it created failing
tests in the e2e suite.
2025-09-10 16:11:16 +02:00
Manuel Raynaud
20161fd6db 🐛(backend) validate user search input data
Only the input data min length was checked. We also have to check the
mex length because the levenshtein dos not accept more than 254
characters and the email field has a max length of 254
2025-09-10 16:11:15 +02:00
dependabot[bot]
e827cfeee1 Bump django from 5.2.4 to 5.2.6 in /src/backend (#1360)
⬆️(backend) bump django from 5.2.4 to 5.2.6
    
Bumps [django](https://github.com/django/django) from 5.2.4 to 5.2.6.
- [Commits](https://github.com/django/django/compare/5.2.4...5.2.6)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.6
  dependency-type: direct:production
...
    
Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 14:09:17 +00:00
Manuel Raynaud
eab2a75bff ♻️(tilt) use hem dev-backend chart (#1340)
Remove usage of bitnami charts and use our own dev-backend charts
instead.
2025-09-10 11:43:30 +00:00
Cyril
cd84751cb9 (frontend) fix major accessibility issues found by wave and axe
improves a11y by fixing multiple critical validation errors

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-10 10:26:47 +02:00
Anthony LC
1d20a8b0a7 💄(frontend) remove margin from modal title
Recent improvement changes the modal title with
a h1 tag, h1 tag adds margin by default.
We remove the margin from the h1 tag to stick to
the design system.
2025-09-10 09:35:54 +02:00
Cyril
8a310d004b (frontend) improve modal a11y: structure, labels, and title
added aria-label, structured text in p, and added title for better accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-10 08:10:35 +02:00
Cyril
9f9fae96e5 (frontend) unify tab focus style for better visual consistency
standardizes keyboard focus appearance to improve UI coherence

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-09 18:37:28 +02:00
Cyril
9cb2b6a6fb (frontend) improve accessibility of cdoc content with correct aria tags
added appropriate aria attributes and semantic tags to enhance accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-09 15:25:38 +02:00
Anthony LC
0a1eaa3c40 🚨(i18n) upgrade eslint to v9 with i18n package
We upgraded ESLint to version 9 in the i18n package,
which includes several improvements and fixes.
This change also involves updating the ESLint
configuration files to the new format and ensuring
compatibility with the latest ESLint features.
2025-09-09 12:27:32 +02:00
Anthony LC
da72a1601a 🚨(y-provider) upgrade eslint to v9 with y-provider server
We upgraded ESLint to version 9 in the y-provider server,
which includes several improvements and fixes.
This change also involves updating the ESLint
configuration files to the new format and ensuring
compatibility with the latest ESLint features.
2025-09-09 12:27:32 +02:00
Anthony LC
9a51e02cd7 🚨(e2e) upgrade eslint to v9 with e2e app
We upgraded ESLint to version 9 in the e2e app,
which includes several improvements and fixes.
This change also involves updating the ESLint
configuration files to the new format and ensuring
compatibility with the latest ESLint features.
2025-09-09 12:27:31 +02:00
Anthony LC
4184c339eb 🚨(docs) upgrade eslint to v9 with Docs app
We upgraded ESLint to version 9 in the Docs app,
which includes several improvements and fixes.
This change also involves updating the ESLint
configuration files to the new format and ensuring
compatibility with the latest ESLint features.
2025-09-09 12:27:31 +02:00
Anthony LC
3688591dd1 ⬆️(dependency) upgrade eslint to v9
We upgraded ESLint to version 9 in the
eslint-config-impress package.
We rename it to eslint-plugin-docs.
2025-09-09 11:03:54 +02:00
Sylvain Zimmer
25783182b8 🗑️(convert) cleanup old content route
Remove rout /api/content, there is no more controller behind and is not
used anymore.
2025-09-08 14:25:10 +02:00
Sylvain Zimmer
80a62bcbc1 (convert) improve tests with stricter tests and less ipsum
Use real example data to run convert handler tests.
2025-09-08 14:24:11 +02:00
Sylvain Zimmer
ede0a77665 ♻️(convert) reuse existing convert yprovider endpoint for content API
reuse convert service instead of renaming it in content
2025-09-08 14:23:42 +02:00
Sylvain Zimmer
8a8a1460e5 (api) add API route to fetch document content
This allows API users to process document content, enabling the
use of Docs as a headless CMS for instance, or any kind of document
processing. Fixes #1206.
2025-09-08 14:21:38 +02:00
Manuel Raynaud
0ac9f059b6 🔒️(backend) configure throttle on every viewsets
We want to configure the throttle on all doc's viewsets. In order to
monitor them, we use the MonitoredScopedRateThrottle class and a custom
callback caputing the message in sentry at the warning level.
2025-09-08 09:23:17 +02:00
Manuel Raynaud
179a84150b ⬆️(backend) upgrade django-lasuite to version 0.0.14
To use monitored throttling
2025-09-08 08:16:32 +02:00
Cyril
084d0c1089 (frontend) make delete buttons nvda-accessible
add aria-labels and include close button in title prop so NVDA announces actions

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-05 17:49:18 +02:00
Cyril
c9a6c4d4c6 (frontend) improve placeholder contrast in blocknote for wcag
fixes insufficient contrast

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-05 16:27:33 +02:00
Quentin BEY
9db7d0af8d 🔒️(all) refactor Docker Hub login to use official GitHub actions
Replace custom Docker Hub authentication with standard, secure,
official GitHub actions for improved security and maintainability.

Uses officially supported actions that follow security best practices
and receive regular updates from GitHub.

Avoid unsecure handling of GitHub secrets.

Thanks to @lebaudantoine
2025-09-05 16:05:10 +02:00
171 changed files with 3725 additions and 2260 deletions

View File

@@ -32,7 +32,10 @@ jobs:
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
@@ -65,7 +68,10 @@ jobs:
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main

View File

@@ -8,6 +8,43 @@ and this project adheres to
## [Unreleased]
### Changed
- ♿(frontend) improve accessibility:
- #1354
- ✨fix tree keyboard toggle when children not yet loaded #1388
### Fixed
- 🐛(backend) duplicate sub docs as root for reader users
## [3.7.0] - 2025-09-12
### Added
- ✨(api) add API route to fetch document content #1206
### Changed
- 🔒️(backend) configure throttle on every viewsets #1343
- ⬆️ Bump eslint to V9 #1071
- ♿(frontend) improve accessibility:
- ♿fix major accessibility issues reported by wave and axe #1344
- ✨unify tab focus style for better visual consistency #1341
- ✨improve modal a11y: structure, labels, and title #1349
- ✨improve accessibility of cdoc content with correct aria tags #1271
- ✨unify tab focus style for better visual consistency #1341
- ♿hide decorative icons, label menus, avoid accessible name… #1362
- ♻️(tilt) use helm dev-backend chart
### Removed
- 🔥(frontend) remove multi column drop cursor #1370
### Fixed
- 🐛(frontend) fix callout emoji list #1366
## [3.6.0] - 2025-09-04
### Added
@@ -27,8 +64,10 @@ and this project adheres to
- ♿️(frontend) keyboard interaction with menu #1244
- ♿(frontend) improve header accessibility #1270
- ♿(frontend) improve accessibility for decorative images in editor #1282
- #1338
- #1281
- ♻️(backend) fallback to email identifier when no name #1298
- 🐛(backend) allow ASCII characters in user sub field #1295
- 🐛(backend) allow ASCII characters in user sub field #1295
- ⚡️(frontend) improve fallback width calculation #1333
### Fixed
@@ -41,6 +80,7 @@ and this project adheres to
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
- 🐛(frontend) fix display bug on homepage #1332
- 🐛link role update #1287
- 🔧(keycloak) Fix https required issue in dev mode #1286
## [3.5.0] - 2025-07-31
@@ -712,7 +752,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.6.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.7.0...main
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
[v3.4.2]: https://github.com/suitenumerique/docs/releases/v3.4.2

View File

@@ -440,6 +440,6 @@ bump-packages-version: ## bump the version of the project - VERSION_TYPE can be
cd ./src/frontend/apps/e2e/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/apps/impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/servers/y-provider/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/eslint-config-impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/eslint-plugin-docs/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/i18n/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
.PHONY: bump-packages-version

View File

@@ -54,16 +54,16 @@ Docs is a collaborative text editor designed to address common challenges in kno
We use Kubernetes for our [production instance](https://docs.numerique.gouv.fr/) but also support Docker Compose. The community contributed a couple other methods (Nix, YunoHost etc.) check out the [docs](/docs/installation/README.md) to get detailed instructions and examples.
#### 🌍 Known instances
We hope to see many more, here is an incomplete list of public Docs instances (urls listed in alphabetical order). Feel free to make a PR to add ones that are not listed below🙏
| | | |
| --- | --- | ------- |
We hope to see many more, here is an incomplete list of public Docs instances. Feel free to make a PR to add ones that are not listed below🙏
| Url | Org | Public |
| docs.numerique.gouv.fr | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up|
| docs.suite.anct.gouv.fr | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up|
| notes.demo.opendesk.eu | ZenDiS | Demo instance of OpenDesk. Request access to get credentials |
| notes.liiib.re | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| docs.federated.nexus | federated.nexus | Public instance, but you have to [sign up for a Federated Nexus account](https://federated.nexus/register/). |
| --- | --- | ------- |
| [docs.numerique.gouv.fr](https://docs.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up|
| [docs.suite.anct.gouv.fr](https://docs.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up|
| [notes.demo.opendesk.eu](https://notes.demo.opendesk.eu) | ZenDiS | Demo instance of OpenDesk. Request access to get credentials |
| [notes.liiib.re](https://notes.liiib.re/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| [docs.federated.nexus](https://docs.federated.nexus/) | federated.nexus | Public instance, but you have to [sign up for a Federated Nexus account](https://federated.nexus/register/). |
| [docs.demo.mosacloud.eu](https://docs.demo.mosacloud.eu/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
#### ⚠️ Advanced features
For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under GPL and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.

View File

@@ -39,9 +39,10 @@ docker_build(
]
)
k8s_resource('impress-docs-backend-migrate', resource_deps=['postgres-postgresql'])
k8s_resource('impress-docs-backend-migrate', resource_deps=['dev-backend-postgres'])
k8s_resource('impress-docs-backend-createsuperuser', resource_deps=['impress-docs-backend-migrate'])
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate'])
k8s_resource('dev-backend-keycloak', resource_deps=['dev-backend-keycloak-pg'])
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate', 'dev-backend-redis', 'dev-backend-keycloak', 'dev-backend-postgres', 'dev-backend-minio:statefulset'])
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
migration = '''

View File

@@ -184,22 +184,20 @@ services:
- env.d/development/kc_postgresql.local
keycloak:
image: quay.io/keycloak/keycloak:20.0.1
image: quay.io/keycloak/keycloak:26.3
volumes:
- ./docker/auth/realm.json:/opt/keycloak/data/import/realm.json
command:
- start-dev
- --features=preview
- --import-realm
- --proxy=edge
- --hostname-url=http://localhost:8083
- --hostname-admin-url=http://localhost:8083/
- --hostname=http://localhost:8083
- --hostname-strict=false
- --hostname-strict-https=false
- --health-enabled=true
- --metrics-enabled=true
healthcheck:
test: ["CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready"]
test: ['CMD-SHELL', 'exec 3<>/dev/tcp/localhost/9000; echo -e "GET /health/live HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" >&3; grep "HTTP/1.1 200 OK" <&3']
start_period: 5s
interval: 1s
timeout: 2s
retries: 300

View File

@@ -26,7 +26,7 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "external",
"sslRequired": "none",
"registrationAllowed": true,
"registrationEmailAsUsername": false,
"rememberMe": true,
@@ -2270,7 +2270,7 @@
"cibaInterval": "5",
"realmReusableOtpCode": "false"
},
"keycloakVersion": "20.0.1",
"keycloakVersion": "26.3.2",
"userManagedAccessAllowed": false,
"clientProfiles": {
"profiles": []

View File

@@ -3,3 +3,7 @@ BURST_THROTTLE_RATES="200/minute"
COLLABORATION_API_URL=http://y-provider:4444/collaboration/api/
SUSTAINED_THROTTLE_RATES="200/hour"
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
# Throttle
API_DOCUMENT_THROTTLE_RATE=1000/min
API_CONFIG_THROTTLE_RATE=1000/min

View File

@@ -27,7 +27,6 @@
"@hocuspocus/provider",
"@hocuspocus/server",
"docx",
"eslint",
"fetch-mock",
"node",
"node-fetch",

View File

@@ -128,3 +128,11 @@ class ListDocumentFilter(DocumentFilter):
queryset_method = queryset.filter if bool(value) else queryset.exclude
return queryset_method(link_traces__user=user, link_traces__is_masked=True)
class UserSearchFilter(django_filters.FilterSet):
"""
Custom filter for searching users.
"""
q = django_filters.CharFilter(min_length=5, max_length=254)

View File

@@ -0,0 +1,21 @@
"""Throttling modules for the API."""
from rest_framework.throttling import UserRateThrottle
from sentry_sdk import capture_message
def sentry_monitoring_throttle_failure(message):
"""Log when a failure occurs to detect rate limiting issues."""
capture_message(message, "warning")
class UserListThrottleBurst(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_burst"
class UserListThrottleSustained(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_sustained"

View File

@@ -1,6 +1,7 @@
"""API endpoints"""
# pylint: disable=too-many-lines
import base64
import json
import logging
import uuid
@@ -33,16 +34,25 @@ from lasuite.malware_detection import malware_detection
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
from rest_framework.throttling import UserRateThrottle
from core import authentication, choices, enums, models
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.converter_services import (
ServiceUnavailableError as YProviderServiceUnavailableError,
)
from core.services.converter_services import (
ValidationError as YProviderValidationError,
)
from core.services.converter_services import (
YdocConverter,
)
from core.tasks.mail import send_ask_for_access_mail
from core.utils import extract_attachments, filter_descendants
from . import permissions, serializers, utils
from .filters import DocumentFilter, ListDocumentFilter
from .filters import DocumentFilter, ListDocumentFilter, UserSearchFilter
from .throttling import UserListThrottleBurst, UserListThrottleSustained
logger = logging.getLogger(__name__)
@@ -136,18 +146,6 @@ class Pagination(drf.pagination.PageNumberPagination):
page_size_query_param = "page_size"
class UserListThrottleBurst(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_burst"
class UserListThrottleSustained(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_sustained"
class UserViewSet(
drf.mixins.UpdateModelMixin, viewsets.GenericViewSet, drf.mixins.ListModelMixin
):
@@ -178,12 +176,18 @@ class UserViewSet(
if self.action != "list":
return queryset
filterset = UserSearchFilter(
self.request.GET, queryset=queryset, request=self.request
)
if not filterset.is_valid():
raise drf.exceptions.ValidationError(filterset.errors)
# Exclude all users already in the given document
if document_id := self.request.query_params.get("document_id", ""):
queryset = queryset.exclude(documentaccess__document_id=document_id)
if not (query := self.request.query_params.get("q", "")) or len(query) < 5:
return queryset.none()
filter_data = filterset.form.cleaned_data
query = filter_data["q"]
# For emails, match emails by Levenstein distance to prevent typing errors
if "@" in query:
@@ -360,6 +364,7 @@ class DocumentViewSet(
permission_classes = [
permissions.DocumentPermission,
]
throttle_scope = "document"
queryset = models.Document.objects.select_related("creator").all()
serializer_class = serializers.DocumentSerializer
ai_translate_serializer_class = serializers.AITranslateSerializer
@@ -936,37 +941,64 @@ class DocumentViewSet(
in the payload.
"""
# Get document while checking permissions
document = self.get_object()
document_to_duplicate = self.get_object()
serializer = serializers.DocumentDuplicationSerializer(
data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
with_accesses = serializer.validated_data.get("with_accesses", False)
is_owner_or_admin = document.get_role(request.user) in models.PRIVILEGED_ROLES
user_role = document_to_duplicate.get_role(request.user)
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
base64_yjs_content = document.content
base64_yjs_content = document_to_duplicate.content
# Duplicate the document instance
link_kwargs = (
{"link_reach": document.link_reach, "link_role": document.link_role}
{
"link_reach": document_to_duplicate.link_reach,
"link_role": document_to_duplicate.link_role,
}
if with_accesses
else {}
)
extracted_attachments = set(extract_attachments(document.content))
attachments = list(extracted_attachments & set(document.attachments))
duplicated_document = document.add_sibling(
extracted_attachments = set(extract_attachments(document_to_duplicate.content))
attachments = list(
extracted_attachments & set(document_to_duplicate.attachments)
)
title = capfirst(_("copy of {title}").format(title=document_to_duplicate.title))
if not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
user_role
) < choices.RoleChoices.get_priority(models.RoleChoices.EDITOR):
duplicated_document = models.Document.add_root(
creator=self.request.user,
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
**link_kwargs,
)
models.DocumentAccess.objects.create(
document=duplicated_document,
user=self.request.user,
role=models.RoleChoices.OWNER,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
duplicated_document = document_to_duplicate.add_sibling(
"right",
title=capfirst(_("copy of {title}").format(title=document.title)),
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document,
duplicated_from=document_to_duplicate,
creator=request.user,
**link_kwargs,
)
# Always add the logged-in user as OWNER for root documents
if document.is_root():
if document_to_duplicate.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
@@ -978,7 +1010,7 @@ class DocumentViewSet(
# If accesses should be duplicated, add other users' accesses as per original document
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document
document=document_to_duplicate
).exclude(user=request.user)
accesses_to_create.extend(
@@ -1505,6 +1537,69 @@ class DocumentViewSet(
status=status.HTTP_400_BAD_REQUEST,
)
@drf.decorators.action(
detail=True,
methods=["get"],
url_path="content",
name="Get document content in different formats",
)
def content(self, request, pk=None):
"""
Retrieve document content in different formats (JSON, Markdown, HTML).
Query parameters:
- content_format: The desired output format (json, markdown, html)
Returns:
JSON response with content in the specified format.
"""
document = self.get_object()
content_format = request.query_params.get("content_format", "json").lower()
if content_format not in {"json", "markdown", "html"}:
raise drf.exceptions.ValidationError(
"Invalid format. Must be one of: json, markdown, html"
)
# Get the base64 content from the document
content = None
base64_content = document.content
if base64_content is not None:
# Convert using the y-provider service
try:
yprovider = YdocConverter()
result = yprovider.convert(
base64.b64decode(base64_content),
"application/vnd.yjs.doc",
{
"markdown": "text/markdown",
"html": "text/html",
"json": "application/json",
}[content_format],
)
content = result
except YProviderValidationError as e:
return drf_response.Response(
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
)
except YProviderServiceUnavailableError as e:
logger.error("Error getting content for document %s: %s", pk, e)
return drf_response.Response(
{"error": "Failed to get document content"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return drf_response.Response(
{
"id": str(document.id),
"title": document.title,
"content": content,
"created_at": document.created_at,
"updated_at": document.updated_at,
}
)
class DocumentAccessViewSet(
ResourceAccessViewsetMixin,
@@ -1555,6 +1650,7 @@ class DocumentAccessViewSet(
"document__depth",
)
resource_field_name = "document"
throttle_scope = "document_access"
@cached_property
def document(self):
@@ -1714,6 +1810,7 @@ class TemplateViewSet(
permissions.IsAuthenticatedOrSafe,
permissions.ResourceWithAccessPermission,
]
throttle_scope = "template"
ordering = ["-created_at"]
ordering_fields = ["created_at", "updated_at", "title"]
serializer_class = serializers.TemplateSerializer
@@ -1804,6 +1901,7 @@ class TemplateAccessViewSet(
lookup_field = "pk"
permission_classes = [permissions.ResourceAccessPermission]
throttle_scope = "template_access"
queryset = models.TemplateAccess.objects.select_related("user").all()
resource_field_name = "template"
serializer_class = serializers.TemplateAccessSerializer
@@ -1886,6 +1984,7 @@ class InvitationViewset(
permissions.CanCreateInvitationPermission,
permissions.ResourceWithAccessPermission,
]
throttle_scope = "invitation"
queryset = (
models.Invitation.objects.all()
.select_related("document")
@@ -1964,6 +2063,7 @@ class DocumentAskForAccessViewSet(
permissions.IsAuthenticated,
permissions.ResourceWithAccessPermission,
]
throttle_scope = "document_ask_for_access"
queryset = models.DocumentAskForAccess.objects.all()
serializer_class = serializers.DocumentAskForAccessSerializer
_document = None
@@ -2036,6 +2136,7 @@ class ConfigView(drf.views.APIView):
"""API ViewSet for sharing some public settings."""
permission_classes = [AllowAny]
throttle_scope = "config"
def get(self, request):
"""

View File

@@ -783,6 +783,7 @@ class Document(MP_Node, BaseModel):
"children_list": can_get,
"children_create": can_create_children,
"collaboration_auth": can_get,
"content": can_get,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": can_destroy,

View File

@@ -1,4 +1,4 @@
"""Converter services."""
"""Y-Provider API services."""
from base64 import b64encode
@@ -28,25 +28,44 @@ class YdocConverter:
# Note: Yprovider microservice accepts only raw token, which is not recommended
return f"Bearer {settings.Y_PROVIDER_API_KEY}"
def convert(self, text):
def _request(self, url, data, content_type, accept):
"""Make a request to the Y-Provider API."""
response = requests.post(
url,
data=data,
headers={
"Authorization": self.auth_header,
"Content-Type": content_type,
"Accept": accept,
},
timeout=settings.CONVERSION_API_TIMEOUT,
verify=settings.CONVERSION_API_SECURE,
)
response.raise_for_status()
return response
def convert(
self, text, content_type="text/markdown", accept="application/vnd.yjs.doc"
):
"""Convert a Markdown text into our internal format using an external microservice."""
if not text:
raise ValidationError("Input text cannot be empty")
try:
response = requests.post(
response = self._request(
f"{settings.Y_PROVIDER_API_BASE_URL}{settings.CONVERSION_API_ENDPOINT}/",
data=text,
headers={
"Authorization": self.auth_header,
"Content-Type": "text/markdown",
},
timeout=settings.CONVERSION_API_TIMEOUT,
verify=settings.CONVERSION_API_SECURE,
text,
content_type,
accept,
)
response.raise_for_status()
return b64encode(response.content).decode("utf-8")
if accept == "application/vnd.yjs.doc":
return b64encode(response.content).decode("utf-8")
if accept in {"text/markdown", "text/html"}:
return response.text
if accept == "application/json":
return response.json()
raise ValidationError("Unsupported format")
except requests.RequestException as err:
raise ServiceUnavailableError(
"Failed to connect to conversion service",

View File

@@ -4,6 +4,7 @@ Test document accesses API endpoints for users in impress's core app.
# pylint: disable=too-many-lines
import random
from unittest import mock
from uuid import uuid4
import pytest
@@ -1344,3 +1345,24 @@ def test_api_document_accesses_delete_owners_last_owner_child_team(
assert response.status_code == 204
assert models.DocumentAccess.objects.count() == 1
def test_api_document_accesses_throttling(settings):
"""Test api document accesses throttling."""
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document_access"] = "2/minute"
user = factories.UserFactory()
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(
document=document, user=user, role="administrator"
)
client = APIClient()
client.force_login(user)
for _i in range(2):
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope document_access", "warning"
)

View File

@@ -824,3 +824,29 @@ def test_api_document_invitations_delete_readers_or_editors(via, role, mock_user
response.json()["detail"]
== "You do not have permission to perform this action."
)
def test_api_document_invitations_throttling(settings):
"""Test api document ask for access throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["invitation"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["invitation"] = "2/minute"
user = factories.UserFactory()
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
factories.InvitationFactory(document=document, issuer=user)
client = APIClient()
client.force_login(user)
for _i in range(2):
response = client.get(f"/api/v1.0/documents/{document.id}/invitations/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get(f"/api/v1.0/documents/{document.id}/invitations/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope invitation", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["invitation"] = current_rate

View File

@@ -1,6 +1,7 @@
"""Test API for document ask for access."""
import uuid
from unittest import mock
from django.core import mail
@@ -768,3 +769,35 @@ def test_api_documents_ask_for_access_accept_authenticated_non_root_document(rol
f"/api/v1.0/documents/{child.id}/ask-for-access/{document_ask_for_access.id}/accept/"
)
assert response.status_code == 404
def test_api_document_ask_for_access_throttling(settings):
"""Test api document ask for access throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"][
"document_ask_for_access"
]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document_ask_for_access"] = (
"2/minute"
)
document = DocumentFactory()
DocumentAskForAccessFactory.create_batch(
3, document=document, role=RoleChoices.READER
)
user = UserFactory()
client = APIClient()
client.force_login(user)
for _i in range(2):
response = client.get(f"/api/v1.0/documents/{document.id}/ask-for-access/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get(f"/api/v1.0/documents/{document.id}/ask-for-access/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope document_ask_for_access", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document_ask_for_access"] = (
current_rate
)

View File

@@ -0,0 +1,176 @@
"""
Tests for Documents API endpoint in impress's core app: content
"""
import base64
from unittest.mock import patch
import pytest
import requests
from rest_framework import status
from rest_framework.test import APIClient
from core import factories
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"reach, role",
[
("public", "reader"),
("public", "editor"),
],
)
@patch("core.services.converter_services.YdocConverter.convert")
def test_api_documents_content_public(mock_content, reach, role):
"""Anonymous users should be allowed to access content of public documents."""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
mock_content.return_value = {"some": "data"}
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] == {"some": "data"}
mock_content.assert_called_once_with(
base64.b64decode(document.content),
"application/vnd.yjs.doc",
"application/json",
)
@pytest.mark.parametrize(
"reach, doc_role, user_role",
[
("restricted", "reader", "reader"),
("restricted", "reader", "editor"),
("restricted", "reader", "administrator"),
("restricted", "reader", "owner"),
("restricted", "editor", "reader"),
("restricted", "editor", "editor"),
("restricted", "editor", "administrator"),
("restricted", "editor", "owner"),
("authenticated", "reader", None),
("authenticated", "editor", None),
],
)
@patch("core.services.converter_services.YdocConverter.convert")
def test_api_documents_content_not_public(mock_content, reach, doc_role, user_role):
"""Authenticated users need access to get non-public document content."""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach=reach, link_role=doc_role)
mock_content.return_value = {"some": "data"}
# First anonymous request should fail
client = APIClient()
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
mock_content.assert_not_called()
# Login and try again
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
# If restricted, we still should not have access
if user_role is not None:
assert response.status_code == status.HTTP_403_FORBIDDEN
mock_content.assert_not_called()
# Create an access as a reader. This should unlock the access.
factories.UserDocumentAccessFactory(
document=document, user=user, role=user_role
)
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] == {"some": "data"}
mock_content.assert_called_once_with(
base64.b64decode(document.content),
"application/vnd.yjs.doc",
"application/json",
)
@pytest.mark.parametrize(
"content_format, accept",
[
("markdown", "text/markdown"),
("html", "text/html"),
("json", "application/json"),
],
)
@patch("core.services.converter_services.YdocConverter.convert")
def test_api_documents_content_format(mock_content, content_format, accept):
"""Test that the content endpoint returns a specific format."""
document = factories.DocumentFactory(link_reach="public")
mock_content.return_value = {"some": "data"}
response = APIClient().get(
f"/api/v1.0/documents/{document.id!s}/content/?content_format={content_format}"
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] == {"some": "data"}
mock_content.assert_called_once_with(
base64.b64decode(document.content), "application/vnd.yjs.doc", accept
)
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_invalid_format(mock_request):
"""Test that the content endpoint rejects invalid formats."""
document = factories.DocumentFactory(link_reach="public")
response = APIClient().get(
f"/api/v1.0/documents/{document.id!s}/content/?content_format=invalid"
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_request.assert_not_called()
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_yservice_error(mock_request):
"""Test that service errors are handled properly."""
document = factories.DocumentFactory(link_reach="public")
mock_request.side_effect = requests.RequestException()
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
mock_request.assert_called_once()
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_nonexistent_document(mock_request):
"""Test that accessing a nonexistent document returns 404."""
client = APIClient()
response = client.get(
"/api/v1.0/documents/00000000-0000-0000-0000-000000000000/content/"
)
assert response.status_code == status.HTTP_404_NOT_FOUND
mock_request.assert_not_called()
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_empty_document(mock_request):
"""Test that accessing an empty document returns empty content."""
document = factories.DocumentFactory(link_reach="public", content="")
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] is None
mock_request.assert_not_called()

View File

@@ -293,3 +293,28 @@ def test_api_documents_duplicate_non_root_document(role):
assert duplicated_accesses.count() == 0
assert duplicated_document.is_sibling_of(child)
assert duplicated_document.is_child_of(document)
def test_api_documents_duplicate_reader_non_root_document():
"""
Reader users should be able to duplicate non-root documents but will be
created as a root document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "reader")])
child = factories.DocumentFactory(parent=document)
assert child.get_role(user) == "reader"
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/", format="json"
)
assert response.status_code == 201
duplicated_document = models.Document.objects.get(id=response.json()["id"])
assert duplicated_document.is_root()
assert duplicated_document.accesses.count() == 1
assert duplicated_document.accesses.get(user=user).role == "owner"

View File

@@ -427,3 +427,20 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries
assert result["is_favorite"] is True
else:
assert result["is_favorite"] is False
def test_api_documents_list_throttling(settings):
"""Test api documents throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = "2/minute"
client = APIClient()
for _i in range(2):
response = client.get("/api/v1.0/documents/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get("/api/v1.0/documents/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope document", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = current_rate

View File

@@ -37,6 +37,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"children_list": True,
"collaboration_auth": True,
"cors_proxy": True,
"content": True,
"descendants": True,
"destroy": False,
"duplicate": False,
@@ -113,6 +114,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": False,
# Anonymous user can't favorite a document even with read access
@@ -218,6 +220,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -300,6 +303,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -494,6 +498,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": access.role in ["administrator", "owner"],
"duplicate": True,
"favorite": True,

View File

@@ -81,6 +81,7 @@ def test_api_documents_trashbin_format():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": True,
"duplicate": True,
"favorite": True,

View File

@@ -3,6 +3,7 @@ Test template accesses API endpoints for users in impress's core app.
"""
import random
from unittest import mock
from uuid import uuid4
import pytest
@@ -773,3 +774,26 @@ def test_api_template_accesses_delete_owners_last_owner(via, mock_user_teams):
assert response.status_code == 403
assert models.TemplateAccess.objects.count() == 2
def test_api_template_accesses_throttling(settings):
"""Test api template accesses throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = "2/minute"
template = factories.TemplateFactory()
user = factories.UserFactory()
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
client = APIClient()
client.force_login(user)
for _i in range(2):
response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope template_access", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = current_rate

View File

@@ -218,3 +218,20 @@ def test_api_templates_list_order_param():
assert response_template_ids == templates_ids, (
"created_at values are not sorted from oldest to newest"
)
def test_api_template_throttling(settings):
"""Test api template throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template"] = "2/minute"
client = APIClient()
for _i in range(2):
response = client.get("/api/v1.0/templates/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get("/api/v1.0/templates/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope template", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template"] = current_rate

View File

@@ -3,6 +3,7 @@ Test config API endpoints in the Impress core app.
"""
import json
from unittest.mock import patch
from django.test import override_settings
@@ -174,3 +175,20 @@ def test_api_config_with_original_theme_customization(is_authenticated, settings
theme_customization = json.load(f)
assert content["theme_customization"] == theme_customization
def test_api_config_throttling(settings):
"""Test api config throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["config"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["config"] = "2/minute"
client = APIClient()
for _i in range(2):
response = client.get("/api/v1.0/config/")
assert response.status_code == 200
with patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get("/api/v1.0/config/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope config", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["config"] = current_rate

View File

@@ -194,18 +194,41 @@ def test_api_users_list_query_short_queries():
factories.UserFactory(email="john.lennon@example.com")
response = client.get("/api/v1.0/users/?q=jo")
assert response.status_code == 200
assert response.json() == []
assert response.status_code == 400
assert response.json() == {
"q": ["Ensure this value has at least 5 characters (it has 2)."]
}
response = client.get("/api/v1.0/users/?q=john")
assert response.status_code == 200
assert response.json() == []
assert response.status_code == 400
assert response.json() == {
"q": ["Ensure this value has at least 5 characters (it has 4)."]
}
response = client.get("/api/v1.0/users/?q=john.")
assert response.status_code == 200
assert len(response.json()) == 2
def test_api_users_list_query_long_queries():
"""
Queries longer than 255 characters should return an empty result set.
"""
user = factories.UserFactory(email="paul@example.com")
client = APIClient()
client.force_login(user)
factories.UserFactory(email="john.doe@example.com")
factories.UserFactory(email="john.lennon@example.com")
query = "a" * 244
response = client.get(f"/api/v1.0/users/?q={query}@example.com")
assert response.status_code == 400
assert response.json() == {
"q": ["Ensure this value has at most 254 characters (it has 256)."]
}
def test_api_users_list_query_inactive():
"""Inactive users should not be listed."""
user = factories.UserFactory()

View File

@@ -161,6 +161,7 @@ def test_models_documents_get_abilities_forbidden(
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"content": False,
"destroy": False,
"duplicate": False,
"favorite": False,
@@ -224,6 +225,7 @@ def test_models_documents_get_abilities_reader(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
@@ -289,6 +291,7 @@ def test_models_documents_get_abilities_editor(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
@@ -343,6 +346,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": True,
"duplicate": True,
"favorite": True,
@@ -394,6 +398,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -448,6 +453,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -509,6 +515,7 @@ def test_models_documents_get_abilities_reader_user(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -568,6 +575,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,

View File

@@ -1,4 +1,4 @@
"""Test converter services."""
"""Test y-provider services."""
from base64 import b64decode
from unittest.mock import MagicMock, patch
@@ -84,6 +84,42 @@ def test_convert_full_integration(mock_post, settings):
headers={
"Authorization": "Bearer test-key",
"Content-Type": "text/markdown",
"Accept": "application/vnd.yjs.doc",
},
timeout=5,
verify=False,
)
@patch("requests.post")
def test_convert_full_integration_with_specific_headers(mock_post, settings):
"""Test successful conversion with specific content type and accept headers."""
settings.Y_PROVIDER_API_BASE_URL = "http://test.com/"
settings.Y_PROVIDER_API_KEY = "test-key"
settings.CONVERSION_API_ENDPOINT = "conversion-endpoint"
settings.CONVERSION_API_TIMEOUT = 5
settings.CONVERSION_API_SECURE = False
converter = YdocConverter()
expected_response = "# Test Document\n\nThis is test content."
mock_response = MagicMock()
mock_response.text = expected_response
mock_response.raise_for_status.return_value = None
mock_post.return_value = mock_response
result = converter.convert(
b"test_content", "application/vnd.yjs.doc", "text/markdown"
)
assert result == expected_response
mock_post.assert_called_once_with(
"http://test.com/conversion-endpoint/",
data=b"test_content",
headers={
"Authorization": "Bearer test-key",
"Content-Type": "application/vnd.yjs.doc",
"Accept": "text/markdown",
},
timeout=5,
verify=False,

View File

@@ -8,11 +8,19 @@ NB_OBJECTS = {
DEV_USERS = [
{"username": "impress", "email": "impress@impress.world", "language": "en-us"},
{"username": "user-e2e-webkit", "email": "user@webkit.test", "language": "en-us"},
{"username": "user-e2e-firefox", "email": "user@firefox.test", "language": "en-us"},
{
"username": "user-e2e-webkit",
"email": "user.test@webkit.test",
"language": "en-us",
},
{
"username": "user-e2e-firefox",
"email": "user.test@firefox.test",
"language": "en-us",
},
{
"username": "user-e2e-chromium",
"email": "user@chromium.test",
"email": "user.test@chromium.test",
"language": "en-us",
},
]

View File

@@ -119,8 +119,8 @@ def create_demo(stdout):
first_name = random.choice(first_names)
queue.push(
models.User(
admin_email=f"user{i:d}@example.com",
email=f"user{i:d}@example.com",
admin_email=f"user.test{i:d}@example.com",
email=f"user.test{i:d}@example.com",
password="!",
is_superuser=False,
is_active=True,

View File

@@ -33,9 +33,9 @@ def test_commands_create_demo():
# assert dev users have doc accesses
user = models.User.objects.get(email="impress@impress.world")
assert models.DocumentAccess.objects.filter(user=user).exists()
user = models.User.objects.get(email="user@webkit.test")
user = models.User.objects.get(email="user.test@webkit.test")
assert models.DocumentAccess.objects.filter(user=user).exists()
user = models.User.objects.get(email="user@firefox.test")
user = models.User.objects.get(email="user.test@firefox.test")
assert models.DocumentAccess.objects.filter(user=user).exists()
user = models.User.objects.get(email="user@chromium.test")
user = models.User.objects.get(email="user.test@chromium.test")
assert models.DocumentAccess.objects.filter(user=user).exists()

View File

@@ -142,7 +142,7 @@ class Base(Configuration):
)
# Document images
DOCUMENT_IMAGE_MAX_SIZE = values.Value(
DOCUMENT_IMAGE_MAX_SIZE = values.IntegerValue(
10 * (2**20), # 10MB
environ_name="DOCUMENT_IMAGE_MAX_SIZE",
environ_prefix=None,
@@ -356,6 +356,9 @@ class Base(Configuration):
"PAGE_SIZE": 20,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_CLASSES": [
"lasuite.drf.throttling.MonitoredScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"user_list_sustained": values.Value(
default="180/hour",
@@ -367,8 +370,46 @@ class Base(Configuration):
environ_name="API_USERS_LIST_THROTTLE_RATE_BURST",
environ_prefix=None,
),
"document": values.Value(
default="80/minute",
environ_name="API_DOCUMENT_THROTTLE_RATE",
environ_prefix=None,
),
"document_access": values.Value(
default="50/minute",
environ_name="API_DOCUMENT_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"template": values.Value(
default="30/minute",
environ_name="API_TEMPLATE_THROTTLE_RATE",
environ_prefix=None,
),
"template_access": values.Value(
default="30/minute",
environ_name="API_TEMPLATE_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"invitation": values.Value(
default="60/minute",
environ_name="API_INVITATION_THROTTLE_RATE",
environ_prefix=None,
),
"document_ask_for_access": values.Value(
default="30/minute",
environ_name="API_DOCUMENT_ASK_FOR_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"config": values.Value(
default="30/minute",
environ_name="API_CONFIG_THROTTLE_RATE",
environ_prefix=None,
),
},
}
MONITORED_THROTTLE_FAILURE_CALLBACK = (
"core.api.throttling.sentry_monitoring_throttle_failure"
)
SPECTACULAR_SETTINGS = {
"TITLE": "Impress API",

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -70,7 +70,7 @@ msgstr "Doare korf"
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -225,8 +225,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titl"
@@ -242,155 +242,155 @@ msgstr "Restr"
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, 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:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
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:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
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:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
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:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, 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:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, 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:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, 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:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -70,7 +70,7 @@ msgstr "Typ"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -225,8 +225,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "Titel"
@@ -242,155 +242,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, 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:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
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:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
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:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -70,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
@@ -242,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -70,7 +70,7 @@ msgstr "Tipo de Cuerpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -163,7 +163,7 @@ msgstr "sub (UUID)"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
@@ -225,8 +225,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "título"
@@ -242,155 +242,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
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:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
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:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1195 core/models.py:1195
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:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 11:45\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -70,7 +70,7 @@ msgstr "Type de corps"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -225,8 +225,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titre"
@@ -242,155 +242,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, 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:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
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:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
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:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
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:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, 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:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, 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:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -70,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -225,8 +225,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titolo"
@@ -242,155 +242,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, 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:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -70,7 +70,7 @@ msgstr "Text type"
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -225,8 +225,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titel"
@@ -242,155 +242,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, 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:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Een url bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -70,7 +70,7 @@ msgstr "Tipo de corpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
@@ -242,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -70,7 +70,7 @@ msgstr "Тип сообщения"
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -225,8 +225,8 @@ msgstr "пользователь"
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "заголовок"
@@ -242,155 +242,155 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -70,7 +70,7 @@ msgstr "Vrsta telesa"
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +225,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "naslov"
@@ -242,155 +242,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, 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:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
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:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -70,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
@@ -242,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -70,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
@@ -242,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -70,7 +70,7 @@ msgstr "Тип вмісту"
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -225,8 +225,8 @@ msgstr "користувач"
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "заголовок"
@@ -242,155 +242,155 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -70,7 +70,7 @@ msgstr "正文类型"
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -225,8 +225,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "标题"
@@ -242,155 +242,155 @@ msgstr "文档"
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.6.0"
version = "3.7.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -34,12 +34,12 @@ dependencies = [
"django-countries==7.6.1",
"django-csp==4.0",
"django-filter==25.1",
"django-lasuite[all]==0.0.11",
"django-lasuite[all]==0.0.14",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.4",
"django==5.2.6",
"django-treebeard==4.7.1",
"djangorestframework==3.16.0",
"drf_spectacular==0.28.0",

View File

@@ -10,13 +10,13 @@ WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/yarn.lock ./yarn.lock
COPY ./src/frontend/apps/impress/package.json ./apps/impress/package.json
COPY ./src/frontend/packages/eslint-config-impress/package.json ./packages/eslint-config-impress/package.json
COPY ./src/frontend/packages/eslint-plugin-docs/package.json ./packages/eslint-plugin-docs/package.json
RUN yarn install --frozen-lockfile
COPY .dockerignore ./.dockerignore
COPY ./src/frontend/.prettierrc.js ./.prettierrc.js
COPY ./src/frontend/packages/eslint-config-impress ./packages/eslint-config-impress
COPY ./src/frontend/packages/eslint-plugin-docs ./packages/eslint-plugin-docs
COPY ./src/frontend/apps/impress ./apps/impress
### ---- Front-end builder image ----

View File

@@ -1,9 +0,0 @@
module.exports = {
root: true,
extends: ['impress/playwright'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
ignorePatterns: ['node_modules'],
};

View File

@@ -5,14 +5,19 @@ import { keyCloakSignIn } from './utils-common';
const saveStorageState = async (
browserConfig: FullProject<unknown, unknown>,
) => {
const browserName = browserConfig?.name || 'chromium';
if (!browserConfig) {
throw new Error('No browser config found');
}
const { storageState, ...useConfig } = browserConfig?.use;
const browserName = browserConfig.name || 'chromium';
const { storageState, ...useConfig } = browserConfig.use;
const browser = await chromium.launch();
const context = await browser.newContext(useConfig);
const page = await context.newPage();
try {
// eslint-disable-next-line playwright/no-networkidle
await page.goto('/', { waitUntil: 'networkidle' });
await page.content();
await expect(page.getByText('Docs').first()).toBeVisible();
@@ -45,11 +50,9 @@ const saveStorageState = async (
};
async function globalSetup(config: FullConfig) {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const chromeConfig = config.projects.find((p) => p.name === 'chromium')!;
const firefoxConfig = config.projects.find((p) => p.name === 'firefox')!;
const webkitConfig = config.projects.find((p) => p.name === 'webkit')!;
/* eslint-enable @typescript-eslint/no-non-null-assertion */
await saveStorageState(chromeConfig);
await saveStorageState(webkitConfig);

View File

@@ -50,7 +50,7 @@ test.describe('Config', () => {
await expect(image).toBeVisible();
// Wait for the media-check to be processed
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
// Check src of image

View File

@@ -45,8 +45,8 @@ test.describe('Doc Create', () => {
})
.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('', { timeout: 10000 });
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
@@ -67,8 +67,8 @@ test.describe('Doc Create', () => {
.getByText('New sub-doc')
.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('', { timeout: 10000 });
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();

View File

@@ -1,3 +1,4 @@
/* eslint-disable playwright/no-conditional-expect */
import path from 'path';
import { chromium, expect, test } from '@playwright/test';
@@ -214,7 +215,6 @@ test.describe('Doc Editor', () => {
});
test('it saves the doc when we quit pages', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
// Check the first doc
@@ -226,9 +226,13 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
await page.waitForTimeout(1000);
const urlDoc = page.url();
await page.goto(urlDoc);
// Wait for editor to load
await expect(editor).toBeVisible();
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
});
@@ -279,7 +283,7 @@ test.describe('Doc Editor', () => {
await expect(image).toBeVisible();
// Wait for the media-check to be processed
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
// Check src of image
@@ -397,8 +401,6 @@ test.describe('Doc Editor', () => {
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
@@ -425,8 +427,6 @@ test.describe('Doc Editor', () => {
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
/* eslint-enable playwright/no-conditional-expect */
/* eslint-enable playwright/no-conditional-in-test */
});
});
@@ -467,12 +467,14 @@ test.describe('Doc Editor', () => {
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
@@ -701,8 +703,20 @@ test.describe('Doc Editor', () => {
const emojiButton = calloutBlock.getByRole('button');
await expect(emojiButton).toHaveText('💡');
await emojiButton.click();
await page.locator('button[aria-label="⚠️"]').click();
await expect(emojiButton).toHaveText('⚠️');
// Group smiley
await expect(page.getByRole('button', { name: '🤠' })).toBeVisible();
// Group animals
await page.getByText('Animals & Nature').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🦆' })).toBeVisible();
// Group travel
await page.getByText('Travel & Places').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🚝' })).toBeVisible();
// Group objects
await page.getByText('Objects').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🪇' })).toBeVisible();
// Group symbol
await page.getByText('Symbols').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🛃' })).toBeVisible();
await page.locator('.bn-side-menu > button').last().click();
await page.locator('.mantine-Menu-dropdown > button').last().click();

View File

@@ -29,12 +29,7 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page
.locator('div')
.filter({ hasText: /^Download$/ })
.first(),
).toBeVisible();
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
page.getByText('Download your document in a .docx or .pdf format.'),
).toBeVisible();
@@ -43,9 +38,11 @@ test.describe('Doc Export', () => {
).toBeVisible();
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Close the modal' }),
page.getByRole('button', {
name: 'Close the download modal',
}),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
});
test('it exports the doc with pdf line break', async ({
@@ -86,12 +83,7 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -136,23 +128,13 @@ test.describe('Doc Export', () => {
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.docx`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
@@ -218,11 +200,7 @@ test.describe('Doc Export', () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await expect(
page.getByRole('button', {
name: 'Download',
}),
).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const responseCorsPromise = page.waitForResponse(
(response) =>
@@ -233,11 +211,7 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
})
.click();
void page.getByTestId('doc-export-download-button').click();
const responseCors = await responseCorsPromise;
expect(responseCors.ok()).toBe(true);
@@ -279,21 +253,13 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page.getByRole('button', {
name: 'Download',
}),
).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -330,22 +296,14 @@ test.describe('Doc Export', () => {
.click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
page.getByTestId('doc-open-modal-download-button'),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -392,22 +350,14 @@ test.describe('Doc Export', () => {
.click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
page.getByTestId('doc-open-modal-download-button'),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -443,14 +393,9 @@ test.describe('Doc Export', () => {
})
.click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'domcontentloaded',
});
const input = page.getByLabel('doc title input');
const input = page.locator('.--docs--doc-title-input[role="textbox"]');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await expect(input).toHaveText('', { timeout: 10000 });
await input.click();
await input.fill(randomDocFrench);
await input.blur();
@@ -469,12 +414,7 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page
.getByRole('button', {
name: 'Télécharger',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
@@ -509,19 +449,23 @@ test.describe('Doc Export', () => {
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
await page
.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
)
.fill('interlink-child');
const input = page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
);
const searchContainer = page.locator('.quick-search-container');
await page
.locator('.quick-search-container')
.getByText('interlink-child')
.click();
await input.fill('export-interlink');
const interlink = page.getByRole('link', {
name: 'interlink-child',
await expect(searchContainer).toBeVisible();
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
// We are in docChild, we want to create a link to randomDoc (parent)
await searchContainer.getByText(randomDoc).click();
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('link', {
name: randomDoc,
});
await expect(interlink).toBeVisible();
@@ -536,12 +480,7 @@ test.describe('Doc Export', () => {
})
.click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
@@ -549,6 +488,6 @@ test.describe('Doc Export', () => {
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
expect(pdfData.text).toContain(randomDoc);
});
});

View File

@@ -36,9 +36,8 @@ test.describe('Doc grid dnd', () => {
expect(draggableBoundingBox).toBeDefined();
expect(dropZoneBoundingBox).toBeDefined();
// eslint-disable-next-line playwright/no-conditional-in-test
if (!draggableBoundingBox || !dropZoneBoundingBox) {
throw new Error('Impossible de déterminer la position des éléments');
throw new Error('Unable to determine the position of the elements');
}
await page.mouse.move(
@@ -86,9 +85,8 @@ test.describe('Doc grid dnd', () => {
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
// eslint-disable-next-line playwright/no-conditional-in-test
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
throw new Error('Impossible de déterminer la position des éléments');
throw new Error('Unable to determine the position of the elements');
}
await page.mouse.move(
@@ -137,9 +135,8 @@ test.describe('Doc grid dnd', () => {
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
// eslint-disable-next-line playwright/no-conditional-in-test
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
throw new Error('Impossible de déterminer la position des éléments');
throw new Error('Unable to determine the position of the elements');
}
await page.mouse.move(

View File

@@ -80,9 +80,7 @@ test.describe('Documents Grid mobile', () => {
hasText: 'My mocked document',
});
await expect(
row.locator('[aria-describedby="doc-title"]').nth(0),
).toHaveText('My mocked document');
await expect(row.getByTestId('doc-title')).toHaveText('My mocked document');
});
});
@@ -149,7 +147,7 @@ test.describe('Document grid item options', () => {
await page
.getByRole('button', {
name: 'Confirm deletion',
name: 'Delete document',
})
.click();
@@ -295,7 +293,7 @@ test.describe('Documents Grid', () => {
docs = result.results as SmallDoc[];
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(page.locator('h4').getByText('All docs')).toBeVisible();
await expect(page.locator('h2').getByText('All docs')).toBeVisible();
const thead = page.getByTestId('docs-grid-header');
await expect(thead.getByText(/Name/i)).toBeVisible();

View File

@@ -25,7 +25,7 @@ test.describe('Doc Header', () => {
'It is the card information about the document.',
);
const docTitle = card.getByRole('textbox', { name: 'doc title input' });
const docTitle = card.getByRole('textbox', { name: 'Document title' });
await expect(docTitle).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
@@ -54,7 +54,7 @@ test.describe('Doc Header', () => {
test('it updates the title doc', async ({ page, browserName }) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await expect(docTitle).toBeVisible();
await docTitle.fill('Hello World');
await docTitle.blur();
@@ -66,7 +66,7 @@ test.describe('Doc Header', () => {
browserName,
}) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await expect(docTitle).toBeVisible();
await docTitle.fill('👍 Hello Emoji World');
await docTitle.blur();
@@ -100,7 +100,7 @@ test.describe('Doc Header', () => {
await page
.getByRole('button', {
name: 'Confirm deletion',
name: 'Delete document',
})
.click();
@@ -155,7 +155,9 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByLabel('Share modal');
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
await expect(page.getByText('Share the document')).toBeVisible();
@@ -226,23 +228,27 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByLabel('Share modal');
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
await expect(page.getByText('Share the document')).toBeVisible();
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test@invitation.test').first(),
).toBeVisible();
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
await expect(
invitationCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
await expect(
memberCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
@@ -294,17 +300,18 @@ test.describe('Doc Header', () => {
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test@invitation.test').first(),
).toBeVisible();
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
await expect(
invitationCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
await expect(
memberCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
@@ -314,7 +321,6 @@ test.describe('Doc Header', () => {
page,
browserName,
}) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -349,7 +355,6 @@ test.describe('Doc Header', () => {
});
test('It checks the copy as HTML button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -384,7 +389,6 @@ test.describe('Doc Header', () => {
});
test('it checks the copy link button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -581,7 +585,10 @@ test.describe('Documents Header mobile', () => {
await page.getByLabel('Open the document options').click();
await page.getByLabel('Share').click();
await expect(page.getByLabel('Share modal')).toBeVisible();
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
await expect(page.getByLabel('Share modal')).toBeHidden();
});

View File

@@ -15,7 +15,7 @@ test.describe('Document create member', () => {
});
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
const inputFill = 'user ';
const inputFill = 'user.test';
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/users/?q=${encodeURIComponent(inputFill)}`) &&
@@ -201,7 +201,7 @@ test.describe('Document create member', () => {
await page.getByLabel('Reader').click();
const moreActions = userInvitation.getByRole('button', {
name: 'more_horiz',
name: 'Open invitation actions menu',
});
await moreActions.click();

View File

@@ -151,7 +151,6 @@ test.describe('Document list members', () => {
await expect(soloOwner).toBeVisible();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
const newUserEmail = await addNewMember(page, 0, 'Owner');
@@ -163,13 +162,11 @@ test.describe('Document list members', () => {
await currentUserRole.click();
await expect(soloOwner).toBeHidden();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
await newUserRoles.click();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});

View File

@@ -40,7 +40,6 @@ test.describe('Doc Routing', () => {
});
test('checks 404 on docs/[id] page', async ({ page }) => {
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(300);
await page.goto('/docs/some-unknown-doc');
@@ -61,32 +60,37 @@ test.describe('Doc Routing', () => {
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
const responsePromise = page.route(
/.*\/documents\/.*\/$|users\/me\/$/,
async (route) => {
const request = route.request();
// Wait for the doc link (via its dynamic title) to be visible
const docLink = page.getByRole('link', { name: docTitle });
await expect(docLink).toBeVisible();
if (
request.method().includes('PATCH') ||
request.method().includes('GET')
) {
await route.fulfill({
status: 401,
json: {
detail: 'Log in to access the document',
},
});
} else {
await route.continue();
}
},
// Intercept GET/PATCH requests to return 401
await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => {
const request = route.request();
if (
request.method().includes('PATCH') ||
request.method().includes('GET')
) {
await route.fulfill({
status: 401,
json: { detail: 'Log in to access the document' },
});
} else {
await route.continue();
}
});
// Explicitly wait for a 401 response after clicking
const wait401 = page.waitForResponse(
(resp) =>
resp.status() === 401 &&
/\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()),
);
await page.getByRole('link', { name: '401-doc-parent' }).click();
await docLink.click();
await wait401;
await responsePromise;
await expect(page.getByText('Log in to access the document')).toBeVisible({
await expect(page.getByText('Log in to access the document.')).toBeVisible({
timeout: 10000,
});
});

View File

@@ -33,7 +33,7 @@ test.describe('Document search', () => {
).toBeVisible();
await expect(
page.getByLabel('Search modal').getByText('search'),
page.getByRole('heading', { name: 'Search docs' }),
).toBeVisible();
const inputSearch = page.getByPlaceholder('Type the name of a document');
@@ -79,7 +79,7 @@ test.describe('Document search', () => {
await page.keyboard.press('Control+k');
await expect(
page.getByLabel('Search modal').getByText('search'),
page.getByRole('heading', { name: 'Search docs' }),
).toBeVisible();
await page.keyboard.press('Escape');
@@ -173,12 +173,13 @@ test.describe('Document search', () => {
.getByRole('combobox', { name: 'Quick search input' })
.fill('sub page search');
// Expect to find the first doc
// Expect to find the first and second docs in the results list
const resultsList = page.getByRole('listbox');
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
resultsList.getByRole('option', { name: firstDocTitle }),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
resultsList.getByRole('option', { name: secondDocTitle }),
).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
@@ -195,14 +196,15 @@ test.describe('Document search', () => {
.fill('second');
// Now there is a sub page - expect to have the focus on the current doc
const updatedResultsList = page.getByRole('listbox');
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
updatedResultsList.getByRole('option', { name: secondDocTitle }),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondChildDocTitle),
updatedResultsList.getByRole('option', { name: secondChildDocTitle }),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
updatedResultsList.getByRole('option', { name: firstDocTitle }),
).toBeHidden();
});
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable playwright/no-conditional-in-test */
import { expect, test } from '@playwright/test';
import {
@@ -51,7 +50,7 @@ test.describe('Doc Tree', () => {
await expect(subPageItem).toBeVisible();
await subPageItem.click();
await verifyDocName(page, '');
const input = page.getByRole('textbox', { name: 'doc title input' });
const input = page.getByRole('textbox', { name: 'Document title' });
await input.click();
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
await input.fill(randomDocName);
@@ -197,7 +196,7 @@ test.describe('Doc Tree', () => {
await page.getByText('Move to my docs').click();
await expect(
page.getByRole('textbox', { name: 'doc title input' }),
page.getByRole('textbox', { name: 'Document title' }),
).not.toHaveText(docChild);
const header = page.locator('header').first();
@@ -253,6 +252,46 @@ test.describe('Doc Tree', () => {
page.getByRole('menuitem', { name: 'Move to my docs' }),
).toHaveAttribute('aria-disabled', 'true');
});
test('keyboard navigation with Enter key opens documents', async ({
page,
browserName,
}) => {
// Create a parent document
const [docParent] = await createDoc(
page,
'doc-tree-keyboard-nav',
browserName,
1,
);
await verifyDocName(page, docParent);
// Create a sub-document
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-tree-keyboard-child',
);
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible();
// Test keyboard navigation on root document
const rootItem = page.getByTestId('doc-tree-root-item');
await expect(rootItem).toBeVisible();
// Focus on the root item and press Enter
await rootItem.focus();
await expect(rootItem).toBeFocused();
await page.keyboard.press('Enter');
// Verify we navigated to the root document
await verifyDocName(page, docParent);
await expect(page).toHaveURL(/\/docs\/[^/]+\/?$/);
// Now test keyboard navigation on sub-document
await expect(docTree.getByText(docChild)).toBeVisible();
});
});
test.describe('Doc Tree: Inheritance', () => {

View File

@@ -15,7 +15,6 @@ test.describe('Doc Visibility', () => {
});
test('It checks the copy link button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -119,8 +118,11 @@ test.describe('Doc Visibility: Restricted', () => {
.click();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
await keyCloakSignIn(page, otherBrowser!);
await keyCloakSignIn(page, otherBrowser);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
@@ -151,6 +153,9 @@ test.describe('Doc Visibility: Restricted', () => {
});
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
const username = `user@${otherBrowser}.test`;
await inputSearch.fill(username);
await page.getByRole('option', { name: username }).click();
@@ -174,7 +179,7 @@ test.describe('Doc Visibility: Restricted', () => {
})
.click();
await keyCloakSignIn(page, otherBrowser!);
await keyCloakSignIn(page, otherBrowser);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
@@ -449,7 +454,10 @@ test.describe('Doc Visibility: Authenticated', () => {
.click();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
await keyCloakSignIn(page, otherBrowser);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
@@ -538,7 +546,10 @@ test.describe('Doc Visibility: Authenticated', () => {
.click();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
await keyCloakSignIn(page, otherBrowser);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,

View File

@@ -76,7 +76,6 @@ test.describe('Header', () => {
* La gaufre load a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1500);
await header

View File

@@ -131,7 +131,7 @@ test.describe('Home page', () => {
// Keyclock login page
await expect(
page.locator('.login-pf-page-header').getByText('impress'),
page.locator('.login-pf #kc-header-wrapper').getByText('impress'),
).toBeVisible();
});
});

View File

@@ -27,7 +27,7 @@ export const CONFIG = {
export const overrideConfig = async (
page: Page,
newConfig: { [K in keyof typeof CONFIG]?: unknown },
newConfig: { [_K in keyof typeof CONFIG]?: unknown },
) =>
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
@@ -56,7 +56,7 @@ export const keyCloakSignIn = async (
const password = `password-e2e-${browserName}`;
await expect(
page.locator('.login-pf-page-header').getByText('impress'),
page.locator('.login-pf #kc-header-wrapper').getByText('impress'),
).toBeVisible();
if (await page.getByLabel('Restart login').isVisible()) {
@@ -65,7 +65,7 @@ export const keyCloakSignIn = async (
await page.getByRole('textbox', { name: 'username' }).fill(login);
await page.getByRole('textbox', { name: 'password' }).fill(password);
await page.click('input[type="submit"]', { force: true });
await page.click('button[type="submit"]', { force: true });
};
export const randomName = (name: string, browserName: string, length: number) =>
@@ -101,10 +101,9 @@ export const createDoc = async (
waitUntil: 'networkidle',
});
const input = page.getByLabel('doc title input');
const input = page.getByLabel('Document title');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await input.click();
await input.fill(randomDocs[i]);
await input.blur();
@@ -120,10 +119,11 @@ export const verifyDocName = async (page: Page, docName: string) => {
timeout: 10000,
});
/*replace toHaveText with toContainText to handle cases where emojis or other characters might be added*/
try {
await expect(
page.getByRole('textbox', { name: 'doc title input' }),
).toHaveText(docName);
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(docName);
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}
@@ -172,7 +172,7 @@ export const goToGridDoc = async (
await expect(row).toBeVisible();
const docTitleContent = row.locator('[aria-describedby="doc-title"]').first();
const docTitleContent = row.getByTestId('doc-title').first();
const docTitle = await docTitleContent.textContent();
expect(docTitle).toBeDefined();
@@ -182,9 +182,9 @@ export const goToGridDoc = async (
};
export const updateDocTitle = async (page: Page, title: string) => {
const input = page.getByLabel('doc title input');
await expect(input).toBeVisible();
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('');
await expect(input).toBeVisible();
await input.click();
await input.fill(title);
await input.click();

View File

@@ -8,7 +8,7 @@ export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
fillText: string = 'user ',
fillText: string = 'user.test',
) => {
const responsePromiseSearchUser = page.waitForResponse(
(response) =>

View File

@@ -0,0 +1,20 @@
import { defineConfig } from '@eslint/config-helpers';
import docsPlugin from 'eslint-plugin-docs';
const eslintConfig = defineConfig([
{
files: ['**/*.ts', '**/*.mjs'],
plugins: {
docs: docsPlugin,
},
extends: ['docs/playwright'],
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: ['./tsconfig.json'],
},
},
},
]);
export default eslintConfig;

View File

@@ -1,9 +1,12 @@
{
"name": "app-e2e",
"version": "3.6.0",
"version": "3.7.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
"lint": "eslint",
"install-playwright": "playwright install --with-deps",
"test": "playwright test",
"test:ui": "yarn test --ui",
@@ -15,11 +18,12 @@
"@playwright/test": "1.55.0",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-config-impress": "*",
"eslint-plugin-docs": "*",
"typescript": "*"
},
"dependencies": {
"convert-stream": "1.0.2",
"pdf-parse": "1.1.1"
}
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -12,8 +12,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"incremental": true
},
"include": ["**/*.ts", "**/*.d.ts"],
"include": ["**/*.ts", "**/*.d.ts", "**/*.mjs"],
"exclude": ["node_modules"]
}

View File

@@ -1,5 +1,5 @@
declare module 'convert-stream' {
export function toBuffer(
readableStream: NodeJS.ReadableStream,
_readableStream: NodeJS.ReadableStream,
): Promise<Buffer>;
}

View File

@@ -1,14 +0,0 @@
module.exports = {
root: true,
extends: ['impress/next'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
settings: {
next: {
rootDir: __dirname,
},
},
ignorePatterns: ['node_modules', '.eslintrc.js', 'service-worker.js'],
};

View File

@@ -0,0 +1,24 @@
import { defineConfig } from '@eslint/config-helpers';
import docsPlugin from 'eslint-plugin-docs';
const eslintConfig = defineConfig([
{
plugins: {
docs: docsPlugin,
},
extends: ['docs/next'],
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: ['./tsconfig.json'],
},
},
settings: {
next: {
rootDir: import.meta.dirname,
},
},
},
]);
export default eslintConfig;

View File

@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@@ -1,6 +1,9 @@
{
"name": "app-impress",
"version": "3.6.0",
"version": "3.7.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
@@ -33,22 +36,22 @@
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.2.3",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "10.8.0",
"@tanstack/react-query": "5.85.6",
"@sentry/nextjs": "10.11.0",
"@tanstack/react-query": "5.87.4",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.25",
"docx": "9.5.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.4.0",
"i18next": "25.4.2",
"emoji-regex": "10.5.0",
"i18next": "25.5.2",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.1",
"next": "15.4.7",
"posthog-js": "1.261.0",
"luxon": "3.7.2",
"next": "15.5.3",
"posthog-js": "1.264.2",
"react": "*",
"react-aria-components": "1.12.1",
"react-dom": "*",
@@ -56,14 +59,14 @@
"react-intersection-observer": "9.16.0",
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.5",
"use-debounce": "10.0.6",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.8"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.85.6",
"@tanstack/react-query-devtools": "5.87.4",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.8.0",
"@testing-library/react": "16.3.0",
@@ -75,13 +78,13 @@
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.0.2",
"cross-env": "10.0.0",
"dotenv": "17.2.1",
"eslint-config-impress": "*",
"dotenv": "17.2.2",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "26.1.0",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.23.1",
"stylelint": "16.24.0",
"stylelint-config-standard": "39.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
@@ -89,5 +92,6 @@
"vitest": "3.2.4",
"webpack": "5.101.3",
"workbox-webpack-plugin": "7.1.0"
}
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -28,17 +28,18 @@ const StyledButton = styled(Button)<StyledButtonProps>`
border: none;
background: none;
outline: none;
transition: all 0.2s ease-in-out;
font-weight: 500;
font-size: 0.938rem;
padding: 0;
${({ $css }) => $css};
&:hover {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
}
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: 2px;
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
border-radius: 4px;
transition: none;
}
`;

View File

@@ -110,7 +110,6 @@ export const DropdownMenu = ({
$direction="row"
$align="center"
$position="relative"
aria-controls="menu"
>
<Box>{children}</Box>
<Icon
@@ -125,9 +124,7 @@ export const DropdownMenu = ({
/>
</Box>
) : (
<Box ref={blockButtonRef} aria-controls="menu">
{children}
</Box>
<Box ref={blockButtonRef}>{children}</Box>
)
}
>
@@ -207,14 +204,13 @@ export const DropdownMenu = ({
}
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline: 2px solid var(--c--theme--colors--primary-400);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
}
${isFocused &&
css`
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
`}
@@ -231,6 +227,7 @@ export const DropdownMenu = ({
$theme="greyscale"
$variation={isDisabled ? '400' : '1000'}
iconName={option.icon}
aria-hidden="true"
/>
)}
<Text $variation={isDisabled ? '400' : '1000'}>
@@ -239,7 +236,12 @@ export const DropdownMenu = ({
</Box>
{(option.isSelected ||
selectedValues?.includes(option.value ?? '')) && (
<Icon iconName="check" $size="20px" $theme="greyscale" />
<Icon
iconName="check"
$size="20px"
$theme="greyscale"
aria-hidden="true"
/>
)}
</BoxButton>
{option.showSeparator && (

View File

@@ -32,7 +32,7 @@ export const useDropdownKeyboardNav = ({
.filter((index) => index !== -1);
switch (event.key) {
case 'ArrowDown':
case 'ArrowDown': {
event.preventDefault();
const nextIndex =
focusedIndex < enabledIndices.length - 1 ? focusedIndex + 1 : 0;
@@ -40,8 +40,9 @@ export const useDropdownKeyboardNav = ({
setFocusedIndex(nextIndex);
menuItemRefs.current[nextEnabledIndex]?.focus();
break;
}
case 'ArrowUp':
case 'ArrowUp': {
event.preventDefault();
const prevIndex =
focusedIndex > 0 ? focusedIndex - 1 : enabledIndices.length - 1;
@@ -49,9 +50,10 @@ export const useDropdownKeyboardNav = ({
setFocusedIndex(prevIndex);
menuItemRefs.current[prevEnabledIndex]?.focus();
break;
}
case 'Enter':
case ' ':
case ' ': {
event.preventDefault();
if (focusedIndex >= 0 && focusedIndex < enabledIndices.length) {
const selectedOptionIndex = enabledIndices[focusedIndex];
@@ -62,6 +64,7 @@ export const useDropdownKeyboardNav = ({
}
}
break;
}
case 'Escape':
event.preventDefault();

View File

@@ -1,4 +1,4 @@
export * from './AlertModal';
export * from './modal/AlertModal';
export * from './Box';
export * from './BoxButton';
export * from './Card';
@@ -9,7 +9,7 @@ export * from './Icon';
export * from './InfiniteScroll';
export * from './Link';
export * from './Loading';
export * from './SideModal';
export * from './modal/SideModal';
export * from './separators';
export * from './Text';
export * from './TextErrors';

View File

@@ -2,8 +2,8 @@ import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from './Box';
import { Text } from './Text';
import { Box } from '../Box';
import { Text } from '../Text';
export type AlertModalProps = {
description: ReactNode;
@@ -30,15 +30,23 @@ export const AlertModal = ({
isOpen={isOpen}
size={ModalSize.MEDIUM}
onClose={onClose}
aria-describedby="alert-modal-title"
title={
<Text $size="h6" $align="flex-start" $variation="1000">
<Text
$size="h6"
as="h1"
$margin="0"
id="alert-modal-title"
$align="flex-start"
$variation="1000"
>
{title}
</Text>
}
rightActions={
<>
<Button
aria-label={t('Close the modal')}
aria-label={`${t('Cancel')} - ${title}`}
color="secondary"
fullWidth
onClick={() => onClose()}
@@ -55,12 +63,11 @@ export const AlertModal = ({
</>
}
>
<Box
aria-label={t('Confirmation button')}
className="--docs--alert-modal"
>
<Box className="--docs--alert-modal">
<Box>
<Text $variation="600">{description}</Text>
<Text $variation="600" as="p">
{description}
</Text>
</Box>
</Box>
</Modal>

View File

@@ -0,0 +1,22 @@
import { Button, type ButtonProps } from '@openfun/cunningham-react';
import React from 'react';
import { Box } from '@/components';
const ButtonCloseModal = (props: ButtonProps) => {
return (
<Button
type="button"
size="small"
color="primary-text"
icon={
<Box as="span" aria-hidden="true" className="material-icons-filled">
close
</Box>
}
{...props}
/>
);
};
export default ButtonCloseModal;

View File

@@ -55,7 +55,6 @@ export const QuickSearchInput = ({
</div>
)}
<Command.Input
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus={true}
aria-label={t('Quick search input')}
onClick={(e) => {
@@ -65,6 +64,7 @@ export const QuickSearchInput = ({
role="combobox"
placeholder={placeholder ?? t('Search')}
onValueChange={onFilter}
maxLength={254}
/>
</Box>
{separator && <HorizontalSeparator $withPadding={false} />}

View File

@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
declare module '*.svg' {
import * as React from 'react';

View File

@@ -2,7 +2,8 @@ import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { BoxButton } from '@/components';
import { Box, BoxButton } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import ProConnectImg from '../assets/button-proconnect.svg';
import { useAuth } from '../hooks';
@@ -11,6 +12,7 @@ import { gotoLogin, gotoLogout } from '../utils';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { authenticated } = useAuth();
const { colorsTokens } = useCunninghamTheme();
if (!authenticated) {
return (
@@ -26,14 +28,23 @@ export const ButtonLogin = () => {
}
return (
<Button
onClick={gotoLogout}
color="primary-text"
aria-label={t('Logout')}
className="--docs--button-logout"
<Box
$css={css`
.--docs--button-logout:focus-visible {
box-shadow: 0 0 0 2px ${colorsTokens['primary-400']} !important;
border-radius: 4px;
}
`}
>
{t('Logout')}
</Button>
<Button
onClick={gotoLogout}
color="primary-text"
aria-label={t('Logout')}
className="--docs--button-logout"
>
{t('Logout')}
</Button>
</Box>
);
};

View File

@@ -44,7 +44,6 @@ import {
} from './custom-inline-content';
import XLMultiColumn from './xl-multi-column';
const multiColumnDropCursor = XLMultiColumn?.multiColumnDropCursor;
const multiColumnLocales = XLMultiColumn?.locales;
const withMultiColumn = XLMultiColumn?.withMultiColumn;
@@ -157,7 +156,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
},
uploadFile,
schema: blockNoteSchema,
dropCursor: multiColumnDropCursor,
},
[collabName, lang, provider, uploadFile],
);

View File

@@ -205,7 +205,7 @@ type ItemProps = Omit<ItemDefault, 'onClick'> & {
onClick: (e: React.MouseEvent) => void;
};
interface AIMenuItemTransform {
interface AIMenuItemTransformProps {
action: AITransformActions;
docId: string;
icon?: ReactNode;
@@ -216,7 +216,7 @@ const AIMenuItemTransform = ({
action,
children,
icon,
}: PropsWithChildren<AIMenuItemTransform>) => {
}: PropsWithChildren<AIMenuItemTransformProps>) => {
const { mutateAsync: requestAI, isPending } = useDocAITransform();
const editor = useBlockNoteEditor();
@@ -244,7 +244,7 @@ const AIMenuItemTransform = ({
);
};
interface AIMenuItemTranslate {
interface AIMenuItemTranslateProps {
language: string;
docId: string;
icon?: ReactNode;
@@ -255,7 +255,7 @@ const AIMenuItemTranslate = ({
docId,
icon,
language,
}: PropsWithChildren<AIMenuItemTranslate>) => {
}: PropsWithChildren<AIMenuItemTranslateProps>) => {
const { mutateAsync: requestAI, isPending } = useDocAITranslate();
const editor = useBlockNoteEditor();

View File

@@ -19,10 +19,11 @@ export const ModalConfirmDownloadUnsafe = ({
isOpen
closeOnClickOutside
onClose={() => onClose()}
aria-describedby="modal-confirm-download-unsafe-title"
rightActions={
<>
<Button
aria-label={t('Close the modal')}
aria-label={t('Cancel the download')}
color="secondary"
onClick={() => onClose()}
>
@@ -31,6 +32,7 @@ export const ModalConfirmDownloadUnsafe = ({
<Button
aria-label={t('Download')}
color="danger"
data-testid="modal-download-unsafe-button"
onClick={() => {
if (onConfirm) {
void onConfirm();
@@ -45,11 +47,14 @@ export const ModalConfirmDownloadUnsafe = ({
size={ModalSize.SMALL}
title={
<Text
as="h1"
id="modal-confirm-download-unsafe-title"
$gap="0.7rem"
$size="h6"
$align="flex-start"
$variation="1000"
$direction="row"
$margin="0"
>
<Icon iconName="warning" $theme="warning" />
{t('Warning')}

View File

@@ -7,14 +7,12 @@ import { Box } from '@/components';
interface EmojiPickerProps {
emojiData: EmojiMartData;
categories: string[];
onClickOutside: () => void;
onEmojiSelect: ({ native }: { native: string }) => void;
}
export const EmojiPicker = ({
emojiData,
categories,
onClickOutside,
onEmojiSelect,
}: EmojiPickerProps) => {
@@ -24,14 +22,11 @@ export const EmojiPicker = ({
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
<Picker
data={emojiData}
categories={categories}
locale={i18n.resolvedLanguage}
navPosition="none"
onClickOutside={onClickOutside}
onEmojiSelect={onEmojiSelect}
previewPosition="none"
skinTonePosition="none"
theme="light"
/>
</Box>
);

View File

@@ -10,7 +10,7 @@ import { Box, BoxButton, Icon } from '@/components';
import { DocsBlockNoteEditor } from '../../types';
import { EmojiPicker } from '../EmojiPicker';
import InitEmojiCallout from './initEmojiCallout';
import emojidata from './initEmojiCallout';
export const CalloutBlock = createReactBlockSpec(
{
@@ -79,8 +79,7 @@ export const CalloutBlock = createReactBlockSpec(
{openEmojiPicker && (
<EmojiPicker
emojiData={InitEmojiCallout.emojidata}
categories={InitEmojiCallout.calloutCategories}
emojiData={emojidata}
onClickOutside={onClickOutside}
onEmojiSelect={onEmojiSelect}
/>

View File

@@ -56,21 +56,4 @@ if (!emojidata.categories.some((c) => c.id === CALLOUT_ID)) {
void init({ data: emojidata });
const calloutCategories = [
'callout',
'people',
'nature',
'foods',
'activity',
'places',
'flags',
'objects',
'symbols',
];
const calloutEmojiData = {
emojidata,
calloutCategories,
};
export default calloutEmojiData;
export default emojidata;

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { createReactInlineContentSpec } from '@blocknote/react';
import { TFunction } from 'i18next';

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/rules-of-hooks */
import {
PartialCustomInlineContentFromConfig,
StyleSchema,

View File

@@ -7,6 +7,19 @@ export const cssEditor = (readonly: boolean) => css`
height: 100%;
padding-bottom: 2rem;
/**
* WCAG Accessibility contrast fixes for BlockNote editor
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
color: #767676 !important;
font-weight: 400;
}
.bn-side-menu .mantine-UnstyledButton-root svg {
color: #767676 !important;
}
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable jsx-a11y/alt-text */
import { DefaultProps } from '@blocknote/core';
import { Image, Text, View } from '@react-pdf/renderer';

View File

@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Text } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import { useEditorStore } from '@/docs/doc-editor';
import { Doc, useTrans } from '@/docs/doc-management';
@@ -131,10 +132,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
isOpen
closeOnClickOutside
onClose={() => onClose()}
hideCloseButton
aria-describedby="modal-export-title"
rightActions={
<>
<Button
aria-label={t('Close the modal')}
aria-label={t('Cancel the download')}
color="secondary"
fullWidth
onClick={() => onClose()}
@@ -143,6 +146,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
{t('Cancel')}
</Button>
<Button
data-testid="doc-export-download-button"
aria-label={t('Download')}
color="primary"
fullWidth
@@ -155,9 +159,29 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
}
size={ModalSize.MEDIUM}
title={
<Text $size="h6" $variation="1000" $align="flex-start">
{t('Download')}
</Text>
<Box
$direction="row"
$justify="space-between"
$align="center"
$width="100%"
>
<Text
as="h1"
$margin="0"
id="modal-export-title"
$size="h6"
$variation="1000"
$align="flex-start"
data-testid="modal-export-title"
>
{t('Download')}
</Text>
<ButtonCloseModal
aria-label={t('Close the download modal')}
onClick={() => onClose()}
disabled={isExporting}
/>
</Box>
}
>
<Box
@@ -166,7 +190,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
$gap="1rem"
className="--docs--modal-export-content"
>
<Text $variation="600" $size="sm">
<Text $variation="600" $size="sm" as="p">
{t('Download your document in a .docx or .pdf format.')}
</Text>
<Select

View File

@@ -1,4 +1,3 @@
/* eslint-disable jsx-a11y/alt-text */
import { Image, Link, Text } from '@react-pdf/renderer';
import DocSelectedIcon from '../assets/doc-selected.png';

View File

@@ -1,5 +1,3 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { Tooltip } from '@openfun/cunningham-react';
import React, { useCallback, useEffect, useState } from 'react';
@@ -107,7 +105,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
}, [doc]);
return (
<Tooltip content={t('Rename')} placement="top">
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
<Box
as="span"
role="textbox"
@@ -116,7 +114,8 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
defaultValue={titleDisplay || undefined}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
aria-label="doc title input"
aria-label={`${t('Document title')}`}
aria-multiline={false}
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}

View File

@@ -215,7 +215,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
>
<Button
color="tertiary"
aria-label="Share button"
aria-label={t('Share button')}
icon={
<Icon iconName="group" $theme="primary" $variation="800" />
}
@@ -233,9 +233,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
{!isSmallMobile && ModalExport && (
<Button
data-testid="doc-open-modal-download-button"
color="tertiary-text"
icon={
<Icon iconName="download" $theme="primary" $variation="800" />
<Icon
iconName="download"
$theme="primary"
$variation="800"
aria-hidden={true}
/>
}
onClick={() => {
setIsModalExportOpen(true);
@@ -244,8 +250,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
aria-label={t('Export the document')}
/>
)}
<DropdownMenu options={options}>
<DropdownMenu options={options} label={t('Open the document options')}>
<IconOptions
aria-hidden="true"
isHorizontal
$theme="primary"
$padding={{ all: 'xs' }}
@@ -261,7 +268,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
`
: ''}
`}
aria-label={t('Open the document options')}
/>
</DropdownMenu>
</Box>

View File

@@ -65,24 +65,20 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
return useMutation<DuplicateDocResponse, APIError, DuplicateDocParams>({
mutationFn: async (variables) => {
try {
// Save the document if we can first, to ensure the latest state is duplicated
if (
variables.canSave &&
provider &&
provider.document.guid === variables.docId
) {
await updateDoc({
id: variables.docId,
content: toBase64(Y.encodeStateAsUpdate(provider.document)),
});
}
// Save the document if we can first, to ensure the latest state is duplicated
const canSave =
variables.canSave &&
provider &&
provider.document.guid === variables.docId;
return await duplicateDoc(variables);
} catch (error) {
// If save fails, throw the error to prevent duplication
throw error;
if (canSave) {
await updateDoc({
id: variables.docId,
content: toBase64(Y.encodeStateAsUpdate(provider.document)),
});
}
return await duplicateDoc(variables);
},
onSuccess: (data, variables, context) => {
void queryClient.resetQueries({

Some files were not shown because too many files have changed in this diff Show More