Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony LC
f1b41cc4c4 POC override 2025-11-21 14:28:31 +01:00
251 changed files with 8806 additions and 19239 deletions

View File

@@ -1,24 +0,0 @@
name: 'Free Disk Space'
description: 'Free up disk space by removing large preinstalled items and cleaning up Docker'
runs:
using: "composite"
steps:
- name: Free disk space (Linux only)
if: runner.os == 'Linux'
shell: bash
run: |
echo "Disk usage before cleanup:"
df -h
# Remove large preinstalled items that are not used on GitHub-hosted runners
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/lib/android || true
# Clean up Docker
docker system prune -af || true
docker volume prune -f || true
echo "Disk usage after cleanup:"
df -h

View File

@@ -52,12 +52,6 @@ jobs:
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-
name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f
build-and-push-frontend:
runs-on: ubuntu-latest
@@ -65,6 +59,14 @@ jobs:
-
name: Checkout repository
uses: actions/checkout@v4
- name: Checkout custom code repository
uses: actions/checkout@v4
with:
repository: 'AntoLC/docs-customized'
ref: 'main'
path: docs-custom
-
name: Docker meta
id: meta
@@ -82,7 +84,7 @@ jobs:
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production --build-arg CUSTOM_CODE=docs-custom'
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
-
name: Build and push
@@ -93,16 +95,11 @@ jobs:
target: frontend-production
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
CUSTOM_CODE=docs-custom
PUBLISH_AS_MIT=false
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-
name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f
build-and-push-y-provider:
runs-on: ubuntu-latest
@@ -137,12 +134,6 @@ jobs:
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-
name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f
notify-argocd:
needs:

View File

@@ -85,9 +85,6 @@ jobs:
- name: Install Playwright Browsers
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright chromium
- name: Free disk space before Docker
uses: ./.github/actions/free-disk-space
- name: Start Docker services
run: make bootstrap-e2e FLUSH_ARGS='--no-input'
@@ -127,9 +124,6 @@ jobs:
- name: Install Playwright Browsers
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright firefox webkit chromium
- name: Free disk space before Docker
uses: ./.github/actions/free-disk-space
- name: Start Docker services
run: make bootstrap-e2e FLUSH_ARGS='--no-input'

View File

@@ -19,7 +19,7 @@ jobs:
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: show
@@ -46,7 +46,7 @@ jobs:
github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 50
- name: Check that the CHANGELOG has been modified in the current branch
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
@@ -70,7 +70,7 @@ jobs:
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install codespell
run: pip install --user codespell
- name: Check for typos
@@ -92,7 +92,7 @@ jobs:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install Python
uses: actions/setup-python@v5
with:

4
.gitignore vendored
View File

@@ -43,10 +43,6 @@ venv.bak/
env.d/development/*.local
env.d/terraform
# Docker
compose.override.yml
docker/auth/*.local
# npm
node_modules

View File

@@ -6,72 +6,10 @@ and this project adheres to
## [Unreleased]
## [4.2.0] - 2025-12-17
### Added
- ✨(backend) allow to create a new user in a marketing system #1707
- ✨(backend) add async indexation of documents on save (or access save) #1276
- ✨(backend) add debounce mechanism to limit indexation jobs #1276
- ✨(api) add API route to search for indexed documents in Find #1276
- 🥅(frontend) add boundary error page #1728
### Changed
- 🛂(backend) stop throttling collaboration servers #1730
- 🚸(backend) use unaccented full name for user search #1637
- 🌐(backend) internationalize demo #1644
- ♿(frontend) improve accessibility:
-Improve keyboard accessibility for the document tree #1681
### Fixed
- 🐛(frontend) paste content with comments from another document #1732
- 🐛(frontend) Select text + Go back one page crash the app #1733
- 🐛(frontend) fix versioning conflict #1742
## [4.1.0] - 2025-12-09
### Added
- ⚡️(frontend) export html #1669
### Changed
- ♿(frontend) improve accessibility:
- ♿(frontend) add skip to content button for keyboard accessibility #1624
- ♿(frontend) fix toggle panel button a11y labels #1634
- 🔒️(frontend) remove dangerouslySetInnerHTML from codebase #1712
- ⚡️(frontend) improve Comments feature #1687
### Fixed
- 🐛(nginx) fix / location to handle new static pages #1682
- 🐛(frontend) rerendering during resize window #1715
## [4.0.0] - 2025-12-01
### Added
- ✨ Add comments feature to the editor #1330
- ✨(backend) Comments on text editor #1330
- ✨(frontend) link to create new doc #1574
### Changed
- ⚡️(sw) stop to cache external resources likes videos #1655
- 💥(frontend) upgrade to ui-kit v2 #1605
- ⚡️(frontend) improve perf on upload and table of contents #1662
- ♿(frontend) improve accessibility:
- ♿(frontend) improve share modal button accessibility #1626
- ♿(frontend) improve screen reader support in DocShare modal #1628
### Fixed
- 🐛(frontend) fix toolbar not activated when reader #1640
- 🐛(frontend) preserve left panel width on window resize #1588
- 🐛(frontend) prevent duplicate as first character in title #1595
## [3.10.0] - 2025-11-18
@@ -102,11 +40,13 @@ and this project adheres to
### Security
- mitigate role escalation in the ask_for_access viewset #1580
- 🐛(frontend) preserve left panel width on window resize #1588
### Removed
- 🔥(backend) remove api managing templates
## [3.9.0] - 2025-11-10
### Added
@@ -114,6 +54,7 @@ and this project adheres to
- ✨(frontend) create skeleton component for DocEditor #1491
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
- ✨(frontend) ajustable left panel #1456
- ✨ Add comments feature to the editor #1330
### Changed
@@ -242,6 +183,7 @@ and this project adheres to
### Added
- ✨(backend) Comments on text editor #1309
- 👷(CI) add bundle size check job #1268
- ✨(frontend) use title first emoji as doc icon in tree #1289
@@ -944,10 +886,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.2.0...main
[v4.2.0]: https://github.com/suitenumerique/docs/releases/v4.2.0
[v4.1.0]: https://github.com/suitenumerique/docs/releases/v4.1.0
[v4.0.0]: https://github.com/suitenumerique/docs/releases/v4.0.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.10.0...main
[v3.10.0]: https://github.com/suitenumerique/docs/releases/v3.10.0
[v3.9.0]: https://github.com/suitenumerique/docs/releases/v3.9.0
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2

View File

@@ -247,10 +247,6 @@ demo: ## flush db then create a demo for load testing purpose
@$(MANAGE) create_demo
.PHONY: demo
index: ## index all documents to remote search
@$(MANAGE) index
.PHONY: index
# Nota bene: Black should come after isort just in case they don't agree...
lint: ## lint back-end python sources
lint: \

View File

@@ -16,29 +16,6 @@ the following command inside your docker container:
## [Unreleased]
## [4.0.0] - 2025-11-26
- ⚠️ We updated `@gouvfr-lasuite/ui-kit` to `0.18.0`, so if you are customizing Docs with a css layer or with a custom template, you need to update your customization to follow the new design system structure.
More information about the changes in the design system can be found here:
- https://suitenumerique.github.io/cunningham/storybook/?path=/docs/migrating-from-v3-to-v4--docs
- https://github.com/suitenumerique/docs/pull/1605
- https://github.com/suitenumerique/docs/blob/main/docs/theming.md
- If you were using the `THEME_CUSTOMIZATION_FILE_PATH` and have overridden the header logo, you need to update your customization file to follow the new structure of the header, it is now:
```json
{
...,
"header": {
"icon": {
"src": "your_logo_src",
"width": "your_logo_width",
"height": "your_logo_height"
}
}
}
```
## [3.3.0] - 2025-05-22
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 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

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
# shellcheck source=bin/_config.sh
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
_dc_run app-dev python -c 'from cryptography.fernet import Fernet;import sys; sys.stdout.write("\n" + Fernet.generate_key().decode() + "\n");'

View File

@@ -72,11 +72,6 @@ services:
- env.d/development/postgresql.local
ports:
- "8071:8000"
networks:
default: {}
lasuite:
aliases:
- impress
volumes:
- ./src/backend:/app
- ./data/static:/data/static
@@ -97,9 +92,6 @@ services:
command: ["celery", "-A", "impress.celery_app", "worker", "-l", "DEBUG"]
environment:
- DJANGO_CONFIGURATION=Development
networks:
- default
- lasuite
env_file:
- env.d/development/common
- env.d/development/common.local
@@ -115,11 +107,6 @@ services:
image: nginx:1.25
ports:
- "8083:8083"
networks:
default: {}
lasuite:
aliases:
- nginx
volumes:
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
@@ -230,8 +217,3 @@ services:
kc_postgresql:
condition: service_healthy
restart: true
networks:
lasuite:
name: lasuite-network
driver: bridge

View File

@@ -12,7 +12,6 @@ flowchart TD
Back --> DB("Database (PostgreSQL)")
Back <--> Celery --> DB
Back ----> S3("Minio (S3)")
Back -- REST API --> Find
```
### Architecture decision records

View File

@@ -6,112 +6,103 @@ Here we describe all environment variables that can be set for the docs applicat
These are the environment variables you can set for the `impress-backend` container.
| Option | Description | default |
|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_MODEL | AI Model to use | |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
| COLLABORATION_API_URL | Collaboration api host | |
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
| COLLABORATION_WS_URL | Collaboration websocket url | |
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert |
| CONVERSION_API_SECURE | Require secure conversion api | false |
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
| CRISP_WEBSITE_ID | Crisp website id for support | |
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
| DB_HOST | Host of the database | localhost |
| DB_NAME | Name of the database | impress |
| DB_PASSWORD | Password to authenticate with | pass |
| DB_PORT | Port of the database | 5432 |
| DB_USER | User to authenticate with | dinum |
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 |
| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_BRAND_NAME | Brand name for email | |
| DJANGO_EMAIL_FROM | Email address used as sender | from@example.com |
| DJANGO_EMAIL_HOST | Hostname of email | |
| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | |
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
| DJANGO_SECRET_KEY | Secret key | |
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
| FRONTEND_THEME | Frontend theme to use | |
| LANGUAGE_CODE | Default language | en-us |
| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend |
| LASUITE_MARKETING_PARAMETERS | The parameters to configure LASUITE_MARKETING_BACKEND. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | {} |
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGIN_REDIRECT_URL | Login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | |
| LOGOUT_REDIRECT_URL | Logout redirect url | |
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
| MEDIA_BASE_URL | | |
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
| OIDC_CREATE_USER | Create used on OIDC | false |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress |
| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | |
| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email |
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
| OIDC_USE_NONCE | Use nonce for OIDC | true |
| POSTHOG_KEY | Posthog key for analytics | |
| REDIS_URL | Cache url | redis://redis:6379/1 |
| SEARCH_INDEXER_BATCH_SIZE | Size of each batch for indexation of all documents | 100000 |
| SEARCH_INDEXER_CLASS | Class of the backend for document indexation & search | |
| SEARCH_INDEXER_COUNTDOWN | Minimum debounce delay of indexation jobs (in seconds) | 1 |
| SEARCH_INDEXER_QUERY_LIMIT | Maximum number of results expected from search endpoint | 50 |
| SEARCH_INDEXER_SECRET | Token for indexation queries | |
| SEARCH_INDEXER_URL | Find application endpoint for indexation | |
| SENTRY_DSN | Sentry host | |
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Register new user to the marketing onboarding. If True, see env LASUITE_MARKETING_* system | False |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
| Y_PROVIDER_API_KEY | Y provider API key | |
| Option | Description | default |
|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_MODEL | AI Model to use | |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
| COLLABORATION_API_URL | Collaboration api host | |
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
| COLLABORATION_WS_URL | Collaboration websocket url | |
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert |
| CONVERSION_API_SECURE | Require secure conversion api | false |
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
| CRISP_WEBSITE_ID | Crisp website id for support | |
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
| DB_HOST | Host of the database | localhost |
| DB_NAME | Name of the database | impress |
| DB_PASSWORD | Password to authenticate with | pass |
| DB_PORT | Port of the database | 5432 |
| DB_USER | User to authenticate with | dinum |
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] |
| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] |
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_BRAND_NAME | Brand name for email | |
| DJANGO_EMAIL_FROM | Email address used as sender | from@example.com |
| DJANGO_EMAIL_HOST | Hostname of email | |
| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | |
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
| DJANGO_SECRET_KEY | Secret key | |
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
| FRONTEND_THEME | Frontend theme to use | |
| LANGUAGE_CODE | Default language | en-us |
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGIN_REDIRECT_URL | Login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | |
| LOGOUT_REDIRECT_URL | Logout redirect url | |
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
| MEDIA_BASE_URL | | |
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
| OIDC_CREATE_USER | Create used on OIDC | false |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress |
| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | |
| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email |
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
| OIDC_USE_NONCE | Use nonce for OIDC | true |
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
| POSTHOG_KEY | Posthog key for analytics | |
| REDIS_URL | Cache url | redis://redis:6379/1 |
| SENTRY_DSN | Sentry host | |
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
| Y_PROVIDER_API_KEY | Y provider API key | |
## impress-frontend image

View File

@@ -1,12 +1,3 @@
djangoSecretKey: &djangoSecretKey "lkjsdlfkjsldkfjslkdfjslkdjfslkdjf"
djangoSuperUserEmail: admin@example.com
djangoSuperUserPass: admin
aiApiKey: changeme
aiBaseUrl: changeme
oidc:
clientId: impress
clientSecret: ThisIsAnExampleKeyForDevPurposeOnly
image:
repository: lasuite/impress-backend
pullPolicy: Always
@@ -15,98 +6,86 @@ image:
backend:
replicas: 1
envVars:
COLLABORATION_API_URL: https://impress.127.0.0.1.nip.io/collaboration/api/
COLLABORATION_SERVER_SECRET: my-secret
DJANGO_CSRF_TRUSTED_ORIGINS: https://docs.127.0.0.1.nip.io
DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io
DJANGO_CONFIGURATION: Feature
DJANGO_ALLOWED_HOSTS: docs.127.0.0.1.nip.io
DJANGO_ALLOWED_HOSTS: impress.127.0.0.1.nip.io
DJANGO_SERVER_TO_SERVER_API_TOKENS: secret-api-key
DJANGO_SECRET_KEY: *djangoSecretKey
DJANGO_SECRET_KEY: AgoodOrAbadKey
DJANGO_SETTINGS_MODULE: impress.settings
DJANGO_SUPERUSER_PASSWORD: admin
DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique"
DJANGO_EMAIL_HOST: "mailcatcher"
DJANGO_EMAIL_LOGO_IMG: https://docs.127.0.0.1.nip.io/assets/logo-suite-numerique.png
DJANGO_EMAIL_LOGO_IMG: https://impress.127.0.0.1.nip.io/assets/logo-suite-numerique.png
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_USE_SSL: False
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
LOGGING_LEVEL_LOGGERS_ROOT: INFO
LOGGING_LEVEL_LOGGERS_APP: INFO
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/logout
OIDC_RP_CLIENT_ID: docs
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
LOGIN_REDIRECT_URL: https://docs.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://docs.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://docs.127.0.0.1.nip.io
DB_HOST: postgresql-dev-backend-postgres
DB_NAME:
secretKeyRef:
name: postgresql-dev-backend-postgres
key: database
DB_USER:
secretKeyRef:
name: postgresql-dev-backend-postgres
key: username
DB_PASSWORD:
secretKeyRef:
name: postgresql-dev-backend-postgres
key: password
OIDC_VERIFY_SSL: False
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
OIDC_REDIRECT_ALLOWED_HOSTS: https://impress.127.0.0.1.nip.io
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://impress.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://impress.127.0.0.1.nip.io
POSTHOG_KEY: "{'id': 'posthog_key', 'host': 'https://product.impress.127.0.0.1.nip.io'}"
DB_HOST: postgresql
DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
REDIS_URL: redis://user:pass@redis-dev-backend-redis:6379/1
DJANGO_CELERY_BROKER_URL: redis://user:pass@redis-dev-backend-redis:6379/1
AWS_S3_ENDPOINT_URL: http://minio-dev-backend-minio.impress.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: dinum
REDIS_URL: redis://default:pass@redis-master:6379/1
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: root
AWS_S3_SECRET_ACCESS_KEY: password
AWS_STORAGE_BUCKET_NAME: docs-media-storage
AWS_STORAGE_BUCKET_NAME: impress-media-storage
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
Y_PROVIDER_API_BASE_URL: http://impress-y-provider:443/api/
Y_PROVIDER_API_KEY: my-secret
CACHES_KEY_PREFIX: "{{ now | unixEpoch }}"
migrate:
command:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py migrate --no-input
python manage.py migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
command:
- "gunicorn"
- "-c"
- "/usr/local/etc/gunicorn/impress.py"
- "impress.wsgi:application"
- "--reload"
createsuperuser:
command:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /cert/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumes:
- name: certs
configMap:
@@ -115,7 +94,12 @@ backend:
- key: cacert.pem
path: cacert.pem
frontend:
envVars:
PORT: 8080
NEXT_PUBLIC_API_ORIGIN: https://impress.127.0.0.1.nip.io
replicas: 1
image:
repository: lasuite/impress-frontend
pullPolicy: Always
@@ -130,47 +114,60 @@ yProvider:
tag: "latest"
envVars:
COLLABORATION_BACKEND_BASE_URL: https://docs.127.0.0.1.nip.io
COLLABORATION_LOGGING: true
COLLABORATION_SERVER_ORIGIN: https://docs.127.0.0.1.nip.io
COLLABORATION_SERVER_ORIGIN: https://impress.127.0.0.1.nip.io
COLLABORATION_SERVER_SECRET: my-secret
Y_PROVIDER_API_KEY: my-secret
COLLABORATION_BACKEND_BASE_URL: https://impress.127.0.0.1.nip.io
NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/cacert.pem
ingress:
enabled: true
host: docs.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 100m
# Mount the certificate so yProvider can establish tls with the backend
extraVolumeMounts:
- name: certs
mountPath: /usr/local/share/ca-certificates/cacert.pem
subPath: cacert.pem
ingressCollaborationWS:
enabled: true
host: docs.127.0.0.1.nip.io
ingressCollaborationApi:
enabled: true
host: docs.127.0.0.1.nip.io
ingressAdmin:
enabled: true
host: docs.127.0.0.1.nip.io
extraVolumes:
- name: certs
configMap:
name: certifi
items:
- key: cacert.pem
path: cacert.pem
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
ingress:
enabled: true
host: impress.127.0.0.1.nip.io
ingressCollaborationWS:
enabled: true
host: impress.127.0.0.1.nip.io
ingressCollaborationApi:
enabled: true
host: impress.127.0.0.1.nip.io
ingressAdmin:
enabled: true
host: impress.127.0.0.1.nip.io
ingressMedia:
enabled: true
host: docs.127.0.0.1.nip.io
host: impress.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/auth-url: https://docs.127.0.0.1.nip.io/api/v1.0/documents/media-auth/
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: minio-dev-backend-minio.impress.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /docs-media-storage/$1
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /impress-media-storage/$1
serviceMedia:
host: minio-dev-backend-minio.impress.svc.cluster.local
host: minio.impress.svc.cluster.local
port: 9000

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,8 @@
minio:
auth:
rootUser: root
rootPassword: password
provisioning:
enabled: true
image: minio/minio
name: minio
# serviceNameOverride: docs-minio
ingress:
enabled: true
hostname: docs-minio.127.0.0.1.nip.io
tls:
enabled: true
secretName: docs-tls
consoleIngress:
enabled: true
hostname: docs-minio-console.127.0.0.1.nip.io
tls:
enabled: true
secretName: docs-tls
api:
port: 80
username: dinum
password: password
bucket: docs-media-storage
versioning: true
size: 1Gi
buckets:
- name: impress-media-storage
versioning: true

View File

@@ -1,9 +1,7 @@
postgres:
enabled: true
name: postgres
#serviceNameOverride: postgres
image: postgres:16-alpine
auth:
username: dinum
password: pass
database: dinum
size: 1Gi
database: impress
tls:
enabled: true
autoGenerated: true

View File

@@ -1,7 +1,4 @@
redis:
enabled: true
name: redis
#serviceNameOverride: redis
image: redis:8.2-alpine
username: user
password: pass
auth:
password: pass
architecture: standalone

View File

@@ -7,7 +7,7 @@ This document is a step-by-step guide that describes how to install Docs on a k8
- k8s cluster with an nginx-ingress controller
- an OIDC provider (if you don't have one, we provide an example)
- a PostgreSQL server (if you don't have one, we provide an example)
- a Redis server (if you don't have one, we provide an example)
- a Memcached server (if you don't have one, we provide an example)
- a S3 bucket (if you don't have one, we provide an example)
### Test cluster
@@ -100,66 +100,50 @@ When your k8s cluster is ready (the ingress nginx controller is up), you can sta
Please remember that `*.127.0.0.1.nip.io` will always resolve to `127.0.0.1`, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
The namespace `impress` is already created, you can work in it and configure your kubectl cli to use it by default.
```
$ kubectl config set-context --current --namespace=impress
```
## Preparation
We provide our own helm chart for all development dependencies, it is available here https://github.com/suitenumerique/helm-dev-backend
This provided chart is for development purpose only and is not ready to use in production.
You can install it on your cluster to deploy keycloak, minio, postgresql and redis.
### What do you use to authenticate your users?
Docs uses OIDC, so if you already have an OIDC provider, obtain the necessary information to use it. In the next step, we will see how to configure Django (and thus Docs) to use it. If you do not have a provider, we will show you how to deploy a local Keycloak instance (this is not a production deployment, just a demo).
```
$ helm install --repo https://suitenumerique.github.io/helm-dev-backend -f docs/examples/helm/keycloak.values.yaml keycloak dev-backend
$ kubectl create namespace impress
$ kubectl config set-context --current --namespace=impress
$ helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak -f examples/keycloak.values.yaml
$ #wait until
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
keycloak-dev-backend-keycloak-0 1/1 Running 0 20s
keycloak-dev-backend-keycloak-pg-0 1/1 Running 0 20s
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 6m48s
keycloak-postgresql-0 1/1 Running 0 6m48s
```
From here the important information you will need are:
```yaml
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
```
You can find these values in **examples/helm/keycloak.values.yaml**
You can find these values in **examples/keycloak.values.yaml**
### Find redis server connection values
Docs needs a redis so we start by deploying one:
```
$ helm install --repo https://suitenumerique.github.io/helm-dev-backend -f docs/examples/helm/redis.values.yaml redis dev-backend
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
keycloak-dev-backend-keycloak-0 1/1 Running 0 113s
keycloak-dev-backend-keycloak-pg-0 1/1 Running 0 113s
redis-dev-backend-redis-68c9f66786-4dgxj 1/1 Running 0 2s
```
From here the important information you will need are:
```yaml
REDIS_URL: redis://user:pass@redis-dev-backend-redis:6379/1
DJANGO_CELERY_BROKER_URL: redis://user:pass@redis-dev-backend-redis:6379/1
$ helm install redis oci://registry-1.docker.io/bitnamicharts/redis -f examples/redis.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 26m
keycloak-postgresql-0 1/1 Running 0 26m
redis-master-0 1/1 Running 0 35s
```
### Find postgresql connection values
@@ -167,32 +151,22 @@ DJANGO_CELERY_BROKER_URL: redis://user:pass@redis-dev-backend-redis:6379/1
Docs uses a postgresql database as backend, so if you have a provider, obtain the necessary information to use it. If you don't, you can install a postgresql testing environment as follow:
```
$ helm install --repo https://suitenumerique.github.io/helm-dev-backend -f docs/examples/helm/postgresql.values.yaml postgresql dev-backend
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
keycloak-dev-backend-keycloak-0 1/1 Running 0 3m42s
keycloak-dev-backend-keycloak-pg-0 1/1 Running 0 3m42s
postgresql-dev-backend-postgres-0 1/1 Running 0 13s
redis-dev-backend-redis-68c9f66786-4dgxj 1/1 Running 0 111s
$ helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql -f examples/postgresql.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 28m
keycloak-postgresql-0 1/1 Running 0 28m
postgresql-0 1/1 Running 0 14m
redis-master-0 1/1 Running 0 42s
```
From here the important information you will need are:
```yaml
DB_HOST: postgresql-dev-backend-postgres
DB_NAME:
secretKeyRef:
name: postgresql-dev-backend-postgres
key: database
DB_USER:
secretKeyRef:
name: postgresql-dev-backend-postgres
key: username
DB_PASSWORD:
secretKeyRef:
name: postgresql-dev-backend-postgres
key: password
DB_HOST: postgres-postgresql
DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
```
@@ -201,15 +175,15 @@ DB_PORT: 5432
Docs uses an s3 bucket to store documents, so if you have a provider obtain the necessary information to use it. If you don't, you can install a local minio testing environment as follow:
```
$ helm install --repo https://suitenumerique.github.io/helm-dev-backend -f docs/examples/helm/minio.values.yaml minio dev-backend
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
keycloak-dev-backend-keycloak-0 1/1 Running 0 6m12s
keycloak-dev-backend-keycloak-pg-0 1/1 Running 0 6m12s
minio-dev-backend-minio-0 1/1 Running 0 10s
postgresql-dev-backend-postgres-0 1/1 Running 0 2m43s
redis-dev-backend-redis-68c9f66786-4dgxj 1/1 Running 0 4m21s
$ helm install minio oci://registry-1.docker.io/bitnamicharts/minio -f examples/minio.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 38m
keycloak-postgresql-0 1/1 Running 0 38m
minio-84f5c66895-bbhsk 1/1 Running 0 42s
minio-provisioning-2b5sq 0/1 Completed 0 42s
postgresql-0 1/1 Running 0 24m
redis-master-0 1/1 Running 0 10m
```
## Deployment
@@ -219,18 +193,20 @@ Now you are ready to deploy Docs without AI. AI requires more dependencies (Open
```
$ helm repo add impress https://suitenumerique.github.io/docs/
$ helm repo update
$ helm install impress impress/docs -f docs/examples/helm/impress.values.yaml
$ helm install impress impress/docs -f examples/impress.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
impress-docs-backend-8494fb797d-8k8wt 1/1 Running 0 6m45s
impress-docs-celery-worker-764b5dd98f-9qd6v 1/1 Running 0 6m45s
impress-docs-frontend-5b69b65cc4-s8pps 1/1 Running 0 6m45s
impress-docs-y-provider-5fc7ccd8cc-6ttrf 1/1 Running 0 6m45s
keycloak-dev-backend-keycloak-0 1/1 Running 0 24m
keycloak-dev-backend-keycloak-pg-0 1/1 Running 0 24m
minio-dev-backend-minio-0 1/1 Running 0 8m24s
postgresql-dev-backend-postgres-0 1/1 Running 0 20m
redis-dev-backend-redis-68c9f66786-4dgxj 1/1 Running 0 22m
NAME READY STATUS RESTARTS AGE
impress-docs-backend-96558758d-xtkbp 0/1 Running 0 79s
impress-docs-backend-createsuperuser-r7ltc 0/1 Completed 0 79s
impress-docs-backend-migrate-c949s 0/1 Completed 0 79s
impress-docs-frontend-6749f644f7-p5s42 1/1 Running 0 79s
impress-docs-y-provider-6947fd8f54-78f2l 1/1 Running 0 79s
keycloak-0 1/1 Running 0 48m
keycloak-postgresql-0 1/1 Running 0 48m
minio-84f5c66895-bbhsk 1/1 Running 0 10m
minio-provisioning-2b5sq 0/1 Completed 0 10m
postgresql-0 1/1 Running 0 34m
redis-master-0 1/1 Running 0 20m
```
## Test your deployment
@@ -239,15 +215,13 @@ In order to test your deployment you have to log into your instance. If you excl
```
$ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
impress-docs <none> docs.127.0.0.1.nip.io localhost 80, 443 7m9s
impress-docs-admin <none> docs.127.0.0.1.nip.io localhost 80, 443 7m9s
impress-docs-collaboration-api <none> docs.127.0.0.1.nip.io localhost 80, 443 7m9s
impress-docs-media <none> docs.127.0.0.1.nip.io localhost 80, 443 7m9s
impress-docs-ws <none> docs.127.0.0.1.nip.io localhost 80, 443 7m9s
keycloak-dev-backend-keycloak <none> docs-keycloak.127.0.0.1.nip.io localhost 80, 443 24m
minio-dev-backend-minio-api <none> docs-minio.127.0.0.1.nip.io localhost 80, 443 8m48s
minio-dev-backend-minio-console <none> docs-minio-console.127.0.0.1.nip.io localhost 80, 443 8m48s
NAME CLASS HOSTS ADDRESS PORTS AGE
impress-docs <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-admin <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-collaboration-api <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-media <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-ws <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
keycloak <none> keycloak.127.0.0.1.nip.io localhost 80 49m
```
You can use Docs at https://docs.127.0.0.1.nip.io. The provisionning user in keycloak is docs/docs.
You can use Docs at https://impress.127.0.0.1.nip.io. The provisionning user in keycloak is impress/impress.

View File

@@ -1,41 +0,0 @@
# Setup the Find search for Impress
This configuration will enable the fulltext search feature for Docs :
- Each save on **core.Document** or **core.DocumentAccess** will trigger the indexer
- The `api/v1.0/documents/search/` will work as a proxy with the Find API for fulltext search.
## Create an index service for Docs
Configure a **Service** for Docs application with these settings
- **Name**: `docs`<br>_request.auth.name of the Docs application._
- **Client id**: `impress`<br>_Name of the token audience or client_id of the Docs application._
See [how-to-use-indexer.md](how-to-use-indexer.md) for details.
## Configure settings of Docs
Add those Django settings the Docs application to enable the feature.
```shell
SEARCH_INDEXER_CLASS="core.services.search_indexers.FindDocumentIndexer"
SEARCH_INDEXER_COUNTDOWN=10 # Debounce delay in seconds for the indexer calls.
# The token from service "docs" of Find application (development).
SEARCH_INDEXER_SECRET="find-api-key-for-docs-with-exactly-50-chars-length"
SEARCH_INDEXER_URL="http://find:8000/api/v1.0/documents/index/"
# Search endpoint. Uses the OIDC token for authentication
SEARCH_INDEXER_QUERY_URL="http://find:8000/api/v1.0/documents/search/"
# Maximum number of results expected from the search endpoint
SEARCH_INDEXER_QUERY_LIMIT=50
```
We also need to enable the **OIDC Token** refresh or the authentication will fail quickly.
```shell
# Store OIDC tokens in the session
OIDC_STORE_ACCESS_TOKEN = True # Store the access token in the session
OIDC_STORE_REFRESH_TOKEN = True # Store the encrypted refresh token in the session
OIDC_STORE_REFRESH_TOKEN_KEY = "your-32-byte-encryption-key==" # Must be a valid Fernet key (32 url-safe base64-encoded bytes)
```

View File

@@ -97,17 +97,6 @@ Production deployments differ significantly from development environments. The t
| 5433 | PostgreSQL (Keycloak) |
| 1081 | MailCatcher |
**With fulltext search service**
| Port | Service |
| --------- | --------------------- |
| 8081 | Find (Django) |
| 9200 | Opensearch |
| 9600 | Opensearch admin |
| 5601 | Opensearch dashboard |
| 25432 | PostgreSQL (Find) |
## 6. Sizing Guidelines
**RAM** start at 8 GB dev / 16 GB staging / 32 GB prod. Postgres and Keycloak are the first to OOM; scale them first.

View File

@@ -32,9 +32,9 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
----
# **Your Docs icon** 📝
# **Your logo** 📝
You can add your own Docs icon in the header from the theme customization file.
You can add your own logo in the header from the theme customization file.
### Settings 🔧
@@ -44,9 +44,9 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
### Example of JSON
You can activate it with the `header.icon` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
You can activate it with the `header.logo` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
This configuration is optional. If not set, the default icon will be used.
This configuration is optional. If not set, the default logo will be used.
----

View File

@@ -36,7 +36,6 @@ OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/impress/protocol/openid-connect/c
OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8083/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT=http://nginx:8083/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT=http://nginx:8083/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_INTROSPECTION_ENDPOINT=http://nginx:8083/realms/impress/protocol/openid-connect/token/introspect
OIDC_RP_CLIENT_ID=impress
OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
@@ -50,14 +49,6 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# Store OIDC tokens in the session. Needed by search/ endpoint.
# OIDC_STORE_ACCESS_TOKEN = True
# OIDC_STORE_REFRESH_TOKEN = True # Store the encrypted refresh token in the session.
# Must be a valid Fernet key (32 url-safe base64-encoded bytes)
# To create one, use the bin/fernetkey command.
# OIDC_STORE_REFRESH_TOKEN_KEY="your-32-byte-encryption-key=="
# AI
AI_FEATURE_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
@@ -77,10 +68,4 @@ Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
Y_PROVIDER_API_KEY=yprovider-api-key
# Theme customization
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
# Indexer (disabled)
# SEARCH_INDEXER_CLASS="core.services.search_indexers.SearchIndexer"
SEARCH_INDEXER_SECRET=find-api-key-for-docs-with-exactly-50-chars-length # Key generated by create_demo in Find app.
SEARCH_INDEXER_URL="http://find:8000/api/v1.0/documents/index/"
SEARCH_INDEXER_QUERY_URL="http://find:8000/api/v1.0/documents/search/"
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15

View File

@@ -1013,13 +1013,3 @@ class ThreadSerializer(serializers.ModelSerializer):
if request:
return thread.get_abilities(request.user)
return {}
class SearchDocumentSerializer(serializers.Serializer):
"""Serializer for fulltext search requests through Find application"""
q = serializers.CharField(required=True, allow_blank=False, trim_whitespace=True)
page_size = serializers.IntegerField(
required=False, min_value=1, max_value=50, default=20
)
page = serializers.IntegerField(required=False, min_value=1, default=1)

View File

@@ -1,8 +1,5 @@
"""Throttling modules for the API."""
from django.conf import settings
from lasuite.drf.throttling import MonitoredScopedRateThrottle
from rest_framework.throttling import UserRateThrottle
from sentry_sdk import capture_message
@@ -22,30 +19,3 @@ class UserListThrottleSustained(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_sustained"
class DocumentThrottle(MonitoredScopedRateThrottle):
"""
Throttle for document-related endpoints, with an exception for requests from the
collaboration server.
"""
scope = "document"
def allow_request(self, request, view):
"""
Override to skip throttling for requests from the collaboration server.
Verifies the X-Y-Provider-Key header contains a valid Y_PROVIDER_API_KEY.
Using a custom header instead of Authorization to avoid triggering
authentication middleware.
"""
y_provider_header = request.headers.get("X-Y-Provider-Key", "")
# Check if this is a valid y-provider request and exempt from throttling
y_provider_key = getattr(settings, "Y_PROVIDER_API_KEY", None)
if y_provider_key and y_provider_header == y_provider_key:
return True
return super().allow_request(request, view)

View File

@@ -1,5 +1,4 @@
"""API endpoints"""
# pylint: disable=too-many-lines
import base64
@@ -19,11 +18,10 @@ from django.core.validators import URLValidator
from django.db import connection, transaction
from django.db import models as db
from django.db.models.expressions import RawSQL
from django.db.models.functions import Greatest, Left, Length
from django.db.models.functions import Left, Length
from django.http import Http404, StreamingHttpResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.text import capfirst, slugify
from django.utils.translation import gettext_lazy as _
@@ -34,13 +32,11 @@ from botocore.exceptions import ClientError
from csp.constants import NONE
from csp.decorators import csp_update
from lasuite.malware_detection import malware_detection
from lasuite.oidc_login.decorators import refresh_oidc_access_token
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
from core import authentication, choices, enums, models
from core.api.filters import remove_accents
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.converter_services import (
@@ -52,20 +48,12 @@ from core.services.converter_services import (
from core.services.converter_services import (
YdocConverter,
)
from core.services.search_indexers import (
get_document_indexer,
get_visited_document_ids_of,
)
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, UserSearchFilter
from .throttling import (
DocumentThrottle,
UserListThrottleBurst,
UserListThrottleSustained,
)
from .throttling import UserListThrottleBurst, UserListThrottleSustained
logger = logging.getLogger(__name__)
@@ -200,15 +188,13 @@ class UserViewSet(
queryset = queryset.exclude(documentaccess__document_id=document_id)
filter_data = filterset.form.cleaned_data
query = remove_accents(filter_data["q"])
query = filter_data["q"]
# For emails, match emails by Levenstein distance to prevent typing errors
if "@" in query:
return (
queryset.annotate(
distance=RawSQL(
"levenshtein(unaccent(email::text), %s::text)", (query,)
)
distance=RawSQL("levenshtein(email::text, %s::text)", (query,))
)
.filter(distance__lte=3)
.order_by("distance", "email")[: settings.API_USERS_LIST_LIMIT]
@@ -217,15 +203,11 @@ class UserViewSet(
# Use trigram similarity for non-email-like queries
# For performance reasons we filter first by similarity, which relies on an
# index, then only calculate precise similarity scores for sorting purposes
return (
queryset.annotate(
sim_email=TrigramSimilarity("email", query),
sim_name=TrigramSimilarity("full_name", query),
)
.annotate(similarity=Greatest("sim_email", "sim_name"))
queryset.filter(email__trigram_word_similar=query)
.annotate(similarity=TrigramSimilarity("email", query))
.filter(similarity__gt=0.2)
.order_by("-similarity")[: settings.API_USERS_LIST_LIMIT]
.order_by("-similarity", "email")[: settings.API_USERS_LIST_LIMIT]
)
@drf.decorators.action(
@@ -383,7 +365,6 @@ class DocumentViewSet(
permission_classes = [
permissions.DocumentPermission,
]
throttle_classes = [DocumentThrottle]
throttle_scope = "document"
queryset = models.Document.objects.select_related("creator").all()
serializer_class = serializers.DocumentSerializer
@@ -393,7 +374,6 @@ class DocumentViewSet(
list_serializer_class = serializers.ListDocumentSerializer
trashbin_serializer_class = serializers.ListDocumentSerializer
tree_serializer_class = serializers.ListDocumentSerializer
search_serializer_class = serializers.ListDocumentSerializer
def get_queryset(self):
"""Get queryset performing all annotation and filtering on the document tree structure."""
@@ -1085,83 +1065,6 @@ class DocumentViewSet(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
def _search_simple(self, request, text):
"""
Returns a queryset filtered by the content of the document title
"""
# As the 'list' view we get a prefiltered queryset (deleted docs are excluded)
queryset = self.get_queryset()
filterset = DocumentFilter({"title": text}, queryset=queryset)
if not filterset.is_valid():
raise drf.exceptions.ValidationError(filterset.errors)
queryset = filterset.filter_queryset(queryset)
return self.get_response_for_queryset(
queryset.order_by("-updated_at"),
context={
"request": request,
},
)
def _search_fulltext(self, indexer, request, params):
"""
Returns a queryset from the results the fulltext search of Find
"""
access_token = request.session.get("oidc_access_token")
user = request.user
text = params.validated_data["q"]
queryset = models.Document.objects.all()
# Retrieve the documents ids from Find.
results = indexer.search(
text=text,
token=access_token,
visited=get_visited_document_ids_of(queryset, user),
)
docs_by_uuid = {str(d.pk): d for d in queryset.filter(pk__in=results)}
ordered_docs = [docs_by_uuid[id] for id in results]
page = self.paginate_queryset(ordered_docs)
serializer = self.get_serializer(
page if page else ordered_docs,
many=True,
context={
"request": request,
},
)
return self.get_paginated_response(serializer.data)
@drf.decorators.action(detail=False, methods=["get"], url_path="search")
@method_decorator(refresh_oidc_access_token)
def search(self, request, *args, **kwargs):
"""
Returns a DRF response containing the filtered, annotated and ordered document list.
Applies filtering based on request parameter 'q' from `SearchDocumentSerializer`.
Depending of the configuration it can be:
- A fulltext search through the opensearch indexation app "find" if the backend is
enabled (see SEARCH_INDEXER_CLASS)
- A filtering by the model field 'title'.
The ordering is always by the most recent first.
"""
params = serializers.SearchDocumentSerializer(data=request.query_params)
params.is_valid(raise_exception=True)
indexer = get_document_indexer()
if indexer:
return self._search_fulltext(indexer, request, params=params)
# The indexer is not configured, we fallback on a simple icontains filter by the
# model field 'title'.
return self._search_simple(request, text=params.validated_data["q"])
@drf.decorators.action(detail=True, methods=["get"], url_path="versions")
def versions_list(self, request, *args, **kwargs):
"""

View File

@@ -1,19 +1,11 @@
"""Impress Core application"""
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
# from django.apps import AppConfig
# from django.utils.translation import gettext_lazy as _
class CoreConfig(AppConfig):
"""Configuration class for the impress core app."""
# class CoreConfig(AppConfig):
# """Configuration class for the impress core app."""
name = "core"
app_label = "core"
verbose_name = _("Impress core application")
def ready(self):
"""
Import signals when the app is ready.
"""
# pylint: disable=import-outside-toplevel, unused-import
from . import signals # noqa: PLC0415
# name = "core"
# app_label = "core"
# verbose_name = _("impress core application")

View File

@@ -6,7 +6,6 @@ import os
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from lasuite.marketing.tasks import create_or_update_contact
from lasuite.oidc_login.backends import (
OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend,
)
@@ -58,22 +57,3 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
return self.UserModel.objects.get_user_by_sub_or_email(sub, email)
except DuplicateEmailError as err:
raise SuspiciousOperation(err.message) from err
def post_get_or_create_user(self, user, claims, is_new_user):
"""
Post-processing after user creation or retrieval.
Args:
user (User): The user instance.
claims (dict): The claims dictionary.
is_new_user (bool): Indicates if the user was newly created.
Returns:
- None
"""
if is_new_user and settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL:
create_or_update_contact.delay(
email=user.email, attributes={"DOCS_SOURCE": ["SIGNIN"]}
)

View File

@@ -1,52 +0,0 @@
"""
Handle search setup that needs to be done at bootstrap time.
"""
import logging
import time
from django.core.management.base import BaseCommand, CommandError
from core.services.search_indexers import get_document_indexer
logger = logging.getLogger("docs.search.bootstrap_search")
class Command(BaseCommand):
"""Index all documents to remote search service"""
help = __doc__
def add_arguments(self, parser):
"""Add argument to require forcing execution when not in debug mode."""
parser.add_argument(
"--batch-size",
action="store",
dest="batch_size",
type=int,
default=50,
help="Indexation query batch size",
)
def handle(self, *args, **options):
"""Launch and log search index generation."""
indexer = get_document_indexer()
if not indexer:
raise CommandError("The indexer is not enabled or properly configured.")
logger.info("Starting to regenerate Find index...")
start = time.perf_counter()
batch_size = options["batch_size"]
try:
count = indexer.index(batch_size=batch_size)
except Exception as err:
raise CommandError("Unable to regenerate index") from err
duration = time.perf_counter() - start
logger.info(
"Search index regenerated from %d document(s) in %.2f seconds.",
count,
duration,
)

View File

@@ -1,37 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-20 09:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0026_comments"),
]
operations = [
migrations.RunSQL(
sql="""
CREATE OR REPLACE FUNCTION public.immutable_unaccent(regdictionary, text)
RETURNS text
LANGUAGE c IMMUTABLE PARALLEL SAFE STRICT AS
'$libdir/unaccent', 'unaccent_dict';
CREATE OR REPLACE FUNCTION public.f_unaccent(text)
RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
RETURN public.immutable_unaccent(regdictionary 'public.unaccent', $1);
CREATE INDEX IF NOT EXISTS user_email_unaccent_trgm_idx
ON impress_user
USING gin (f_unaccent(email) gin_trgm_ops);
CREATE INDEX IF NOT EXISTS user_full_name_unaccent_trgm_idx
ON impress_user
USING gin (f_unaccent(full_name) gin_trgm_ops);
""",
reverse_sql="""
DROP INDEX IF EXISTS user_email_unaccent_trgm_idx;
DROP INDEX IF EXISTS user_full_name_unaccent_trgm_idx;
""",
),
]

View File

@@ -432,35 +432,32 @@ class Document(MP_Node, BaseModel):
def save(self, *args, **kwargs):
"""Write content to object storage only if _content has changed."""
super().save(*args, **kwargs)
if self._content:
self.save_content(self._content)
file_key = self.file_key
bytes_content = self._content.encode("utf-8")
def save_content(self, content):
"""Save content to object storage."""
file_key = self.file_key
bytes_content = content.encode("utf-8")
# Attempt to directly check if the object exists using the storage client.
try:
response = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=file_key
)
except ClientError as excpt:
# If the error is a 404, the object doesn't exist, so we should create it.
if excpt.response["Error"]["Code"] == "404":
has_changed = True
# Attempt to directly check if the object exists using the storage client.
try:
response = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=file_key
)
except ClientError as excpt:
# If the error is a 404, the object doesn't exist, so we should create it.
if excpt.response["Error"]["Code"] == "404":
has_changed = True
else:
raise
else:
raise
else:
# Compare the existing ETag with the MD5 hash of the new content.
has_changed = (
response["ETag"].strip('"') != hashlib.md5(bytes_content).hexdigest() # noqa: S324
)
# Compare the existing ETag with the MD5 hash of the new content.
has_changed = (
response["ETag"].strip('"')
!= hashlib.md5(bytes_content).hexdigest() # noqa: S324
)
if has_changed:
content_file = ContentFile(bytes_content)
default_storage.save(file_key, content_file)
if has_changed:
content_file = ContentFile(bytes_content)
default_storage.save(file_key, content_file)
def is_leaf(self):
"""
@@ -906,8 +903,7 @@ class Document(MP_Node, BaseModel):
# Mark all descendants as soft deleted
self.get_descendants().filter(ancestors_deleted_at__isnull=True).update(
ancestors_deleted_at=self.ancestors_deleted_at,
updated_at=self.updated_at,
ancestors_deleted_at=self.ancestors_deleted_at
)
@transaction.atomic

View File

@@ -1,298 +0,0 @@
"""Document search index management utilities and indexers"""
import logging
from abc import ABC, abstractmethod
from collections import defaultdict
from functools import cache
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Subquery
from django.utils.module_loading import import_string
import requests
from core import models, utils
logger = logging.getLogger(__name__)
@cache
def get_document_indexer():
"""Returns an instance of indexer service if enabled and properly configured."""
classpath = settings.SEARCH_INDEXER_CLASS
# For this usecase an empty indexer class is not an issue but a feature.
if not classpath:
logger.info("Document indexer is not configured (see SEARCH_INDEXER_CLASS)")
return None
try:
indexer_class = import_string(settings.SEARCH_INDEXER_CLASS)
return indexer_class()
except ImportError as err:
logger.error("SEARCH_INDEXER_CLASS setting is not valid : %s", err)
except ImproperlyConfigured as err:
logger.error("Document indexer is not properly configured : %s", err)
return None
def get_batch_accesses_by_users_and_teams(paths):
"""
Get accesses related to a list of document paths,
grouped by users and teams, including all ancestor paths.
"""
ancestor_map = utils.get_ancestor_to_descendants_map(
paths, steplen=models.Document.steplen
)
ancestor_paths = list(ancestor_map.keys())
access_qs = models.DocumentAccess.objects.filter(
document__path__in=ancestor_paths
).values("document__path", "user__sub", "team")
access_by_document_path = defaultdict(lambda: {"users": set(), "teams": set()})
for access in access_qs:
ancestor_path = access["document__path"]
user_sub = access["user__sub"]
team = access["team"]
for descendant_path in ancestor_map.get(ancestor_path, []):
if user_sub:
access_by_document_path[descendant_path]["users"].add(str(user_sub))
if team:
access_by_document_path[descendant_path]["teams"].add(team)
return dict(access_by_document_path)
def get_visited_document_ids_of(queryset, user):
"""
Returns the ids of the documents that have a linktrace to the user and NOT owned.
It will be use to limit the opensearch responses to the public documents already
"visited" by the user.
"""
if isinstance(user, AnonymousUser):
return []
qs = models.LinkTrace.objects.filter(user=user)
docs = (
queryset.exclude(accesses__user=user)
.filter(
deleted_at__isnull=True,
ancestors_deleted_at__isnull=True,
)
.filter(pk__in=Subquery(qs.values("document_id")))
.order_by("pk")
.distinct("pk")
)
return [str(id) for id in docs.values_list("pk", flat=True)]
class BaseDocumentIndexer(ABC):
"""
Base class for document indexers.
Handles batching and access resolution. Subclasses must implement both
`serialize_document()` and `push()` to define backend-specific behavior.
"""
def __init__(self):
"""
Initialize the indexer.
"""
self.batch_size = settings.SEARCH_INDEXER_BATCH_SIZE
self.indexer_url = settings.SEARCH_INDEXER_URL
self.indexer_secret = settings.SEARCH_INDEXER_SECRET
self.search_url = settings.SEARCH_INDEXER_QUERY_URL
self.search_limit = settings.SEARCH_INDEXER_QUERY_LIMIT
if not self.indexer_url:
raise ImproperlyConfigured(
"SEARCH_INDEXER_URL must be set in Django settings."
)
if not self.indexer_secret:
raise ImproperlyConfigured(
"SEARCH_INDEXER_SECRET must be set in Django settings."
)
if not self.search_url:
raise ImproperlyConfigured(
"SEARCH_INDEXER_QUERY_URL must be set in Django settings."
)
def index(self, queryset=None, batch_size=None):
"""
Fetch documents in batches, serialize them, and push to the search backend.
Args:
queryset (optional): Document queryset
Defaults to all documents without filter.
batch_size (int, optional): Number of documents per batch.
Defaults to settings.SEARCH_INDEXER_BATCH_SIZE.
"""
last_id = 0
count = 0
queryset = queryset or models.Document.objects.all()
batch_size = batch_size or self.batch_size
while True:
documents_batch = list(
queryset.filter(
id__gt=last_id,
).order_by("id")[:batch_size]
)
if not documents_batch:
break
doc_paths = [doc.path for doc in documents_batch]
last_id = documents_batch[-1].id
accesses_by_document_path = get_batch_accesses_by_users_and_teams(doc_paths)
serialized_batch = [
self.serialize_document(document, accesses_by_document_path)
for document in documents_batch
if document.content or document.title
]
if serialized_batch:
self.push(serialized_batch)
count += len(serialized_batch)
return count
@abstractmethod
def serialize_document(self, document, accesses):
"""
Convert a Document instance to a JSON-serializable format for indexing.
Must be implemented by subclasses.
"""
@abstractmethod
def push(self, data):
"""
Push a batch of serialized documents to the backend.
Must be implemented by subclasses.
"""
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
def search(self, text, token, visited=(), nb_results=None):
"""
Search for documents in Find app.
Ensure the same default ordering as "Docs" list : -updated_at
Returns ids of the documents
Args:
text (str): Text search content.
token (str): OIDC Authentication token.
visited (list, optional):
List of ids of active public documents with LinkTrace
Defaults to settings.SEARCH_INDEXER_BATCH_SIZE.
nb_results (int, optional):
The number of results to return.
Defaults to 50 if not specified.
"""
nb_results = nb_results or self.search_limit
response = self.search_query(
data={
"q": text,
"visited": visited,
"services": ["docs"],
"nb_results": nb_results,
"order_by": "updated_at",
"order_direction": "desc",
},
token=token,
)
return [d["_id"] for d in response]
@abstractmethod
def search_query(self, data, token) -> dict:
"""
Retrieve documents from the Find app API.
Must be implemented by subclasses.
"""
class SearchIndexer(BaseDocumentIndexer):
"""
Document indexer that pushes documents to La Suite Find app.
"""
def serialize_document(self, document, accesses):
"""
Convert a Document to the JSON format expected by La Suite Find.
Args:
document (Document): The document instance.
accesses (dict): Mapping of document ID to user/team access.
Returns:
dict: A JSON-serializable dictionary.
"""
doc_path = document.path
doc_content = document.content
text_content = utils.base64_yjs_to_text(doc_content) if doc_content else ""
return {
"id": str(document.id),
"title": document.title or "",
"content": text_content,
"depth": document.depth,
"path": document.path,
"numchild": document.numchild,
"created_at": document.created_at.isoformat(),
"updated_at": document.updated_at.isoformat(),
"users": list(accesses.get(doc_path, {}).get("users", set())),
"groups": list(accesses.get(doc_path, {}).get("teams", set())),
"reach": document.computed_link_reach,
"size": len(text_content.encode("utf-8")),
"is_active": not bool(document.ancestors_deleted_at),
}
def search_query(self, data, token) -> requests.Response:
"""
Retrieve documents from the Find app API.
Args:
data (dict): search data
token (str): OICD token
Returns:
dict: A JSON-serializable dictionary.
"""
response = requests.post(
self.search_url,
json=data,
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
response.raise_for_status()
return response.json()
def push(self, data):
"""
Push a batch of documents to the Find backend.
Args:
data (list): List of document dictionaries.
"""
response = requests.post(
self.indexer_url,
json=data,
headers={"Authorization": f"Bearer {self.indexer_secret}"},
timeout=10,
)
response.raise_for_status()

View File

@@ -1,33 +0,0 @@
"""
Declare and configure the signals for the impress core application
"""
from functools import partial
from django.db import transaction
from django.db.models import signals
from django.dispatch import receiver
from . import models
from .tasks.search import trigger_batch_document_indexer
@receiver(signals.post_save, sender=models.Document)
def document_post_save(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Asynchronous call to the document indexer at the end of the transaction.
Note : Within the transaction we can have an empty content and a serialization
error.
"""
transaction.on_commit(partial(trigger_batch_document_indexer, instance))
@receiver(signals.post_save, sender=models.DocumentAccess)
def document_access_post_save(sender, instance, created, **kwargs): # pylint: disable=unused-argument
"""
Asynchronous call to the document indexer at the end of the transaction.
"""
if not created:
transaction.on_commit(
partial(trigger_batch_document_indexer, instance.document)
)

View File

@@ -1,95 +0,0 @@
"""Trigger document indexation using celery task."""
from logging import getLogger
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from django_redis.cache import RedisCache
from core import models
from core.services.search_indexers import (
get_document_indexer,
)
from impress.celery_app import app
logger = getLogger(__file__)
@app.task
def document_indexer_task(document_id):
"""Celery Task : Sends indexation query for a document."""
indexer = get_document_indexer()
if indexer:
logger.info("Start document %s indexation", document_id)
indexer.index(models.Document.objects.filter(pk=document_id))
def batch_indexer_throttle_acquire(timeout: int = 0, atomic: bool = True):
"""
Enable the task throttle flag for a delay.
Uses redis locks if available to ensure atomic changes
"""
key = "document-batch-indexer-throttle"
# Redis is used as cache database (not in tests). Use the lock feature here
# to ensure atomicity of changes to the throttle flag.
if isinstance(cache, RedisCache) and atomic:
with cache.locks(key):
return batch_indexer_throttle_acquire(timeout, atomic=False)
# Use add() here :
# - set the flag and returns true if not exist
# - do nothing and return false if exist
return cache.add(key, 1, timeout=timeout)
@app.task
def batch_document_indexer_task(timestamp):
"""Celery Task : Sends indexation query for a batch of documents."""
indexer = get_document_indexer()
if indexer:
queryset = models.Document.objects.filter(
Q(updated_at__gte=timestamp)
| Q(deleted_at__gte=timestamp)
| Q(ancestors_deleted_at__gte=timestamp)
)
count = indexer.index(queryset)
logger.info("Indexed %d documents", count)
def trigger_batch_document_indexer(item):
"""
Trigger indexation task with debounce a delay set by the SEARCH_INDEXER_COUNTDOWN setting.
Args:
document (Document): The document instance.
"""
countdown = int(settings.SEARCH_INDEXER_COUNTDOWN)
# DO NOT create a task if indexation if disabled
if not settings.SEARCH_INDEXER_CLASS:
return
if countdown > 0:
# Each time this method is called during a countdown, we increment the
# counter and each task decrease it, so the index be run only once.
if batch_indexer_throttle_acquire(timeout=countdown):
logger.info(
"Add task for batch document indexation from updated_at=%s in %d seconds",
item.updated_at.isoformat(),
countdown,
)
batch_document_indexer_task.apply_async(
args=[item.updated_at], countdown=countdown
)
else:
logger.info("Skip task for batch document %s indexation", item.pk)
else:
document_indexer_task.apply(args=[item.pk])

View File

@@ -2,7 +2,6 @@
import random
import re
from unittest import mock
from django.core.exceptions import SuspiciousOperation
from django.test.utils import override_settings
@@ -13,10 +12,7 @@ from cryptography.fernet import Fernet
from lasuite.oidc_login.backends import get_oidc_refresh_token
from core import models
from core.authentication.backends import (
OIDCAuthenticationBackend,
create_or_update_contact,
)
from core.authentication.backends import OIDCAuthenticationBackend
from core.factories import UserFactory
pytestmark = pytest.mark.django_db
@@ -513,79 +509,3 @@ def test_authentication_session_tokens(
assert user is not None
assert request.session["oidc_access_token"] == "test-access-token"
assert get_oidc_refresh_token(request.session) == "test-refresh-token"
def test_authentication_post_get_or_create_user_new_user_to_marketing_email(settings):
"""
New user and SIGNUP_NEW_USER_TO_MARKETING_EMAIL enabled should create a contact
in the marketing backend.
"""
user = UserFactory()
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True
klass = OIDCAuthenticationBackend()
with mock.patch.object(
create_or_update_contact, "delay"
) as mock_create_or_update_contact:
klass.post_get_or_create_user(user, {}, True)
mock_create_or_update_contact.assert_called_once_with(
email=user.email, attributes={"DOCS_SOURCE": ["SIGNIN"]}
)
def test_authentication_post_get_or_create_user_new_user_to_marketing_email_disabled(
settings,
):
"""
New user and SIGNUP_NEW_USER_TO_MARKETING_EMAIL disabled should not create a contact
in the marketing backend.
"""
user = UserFactory()
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = False
klass = OIDCAuthenticationBackend()
with mock.patch.object(
create_or_update_contact, "delay"
) as mock_create_or_update_contact:
klass.post_get_or_create_user(user, {}, True)
mock_create_or_update_contact.assert_not_called()
def test_authentication_post_get_or_create_user_existing_user_to_marketing_email(
settings,
):
"""
Existing user and SIGNUP_NEW_USER_TO_MARKETING_EMAIL enabled should not create a contact
in the marketing backend.
"""
user = UserFactory()
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True
klass = OIDCAuthenticationBackend()
with mock.patch.object(
create_or_update_contact, "delay"
) as mock_create_or_update_contact:
klass.post_get_or_create_user(user, {}, False)
mock_create_or_update_contact.assert_not_called()
def test_authentication_post_get_or_create_user_existing_user_to_marketing_email_disabled(
settings,
):
"""
Existing user and SIGNUP_NEW_USER_TO_MARKETING_EMAIL disabled should not create a contact
in the marketing backend.
"""
user = UserFactory()
settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = False
klass = OIDCAuthenticationBackend()
with mock.patch.object(
create_or_update_contact, "delay"
) as mock_create_or_update_contact:
klass.post_get_or_create_user(user, {}, False)
mock_create_or_update_contact.assert_not_called()

View File

@@ -1,65 +0,0 @@
"""
Unit test for `index` command.
"""
from operator import itemgetter
from unittest import mock
from django.core.management import CommandError, call_command
from django.db import transaction
import pytest
from core import factories
from core.services.search_indexers import SearchIndexer
@pytest.mark.django_db
@pytest.mark.usefixtures("indexer_settings")
def test_index():
"""Test the command `index` that run the Find app indexer for all the available documents."""
user = factories.UserFactory()
indexer = SearchIndexer()
with transaction.atomic():
doc = factories.DocumentFactory()
empty_doc = factories.DocumentFactory(title=None, content="")
no_title_doc = factories.DocumentFactory(title=None)
factories.UserDocumentAccessFactory(document=doc, user=user)
factories.UserDocumentAccessFactory(document=empty_doc, user=user)
factories.UserDocumentAccessFactory(document=no_title_doc, user=user)
accesses = {
str(doc.path): {"users": [user.sub]},
str(empty_doc.path): {"users": [user.sub]},
str(no_title_doc.path): {"users": [user.sub]},
}
with mock.patch.object(SearchIndexer, "push") as mock_push:
call_command("index")
push_call_args = [call.args[0] for call in mock_push.call_args_list]
# called once but with a batch of docs
mock_push.assert_called_once()
assert sorted(push_call_args[0], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc, accesses),
indexer.serialize_document(no_title_doc, accesses),
],
key=itemgetter("id"),
)
@pytest.mark.django_db
@pytest.mark.usefixtures("indexer_settings")
def test_index_improperly_configured(indexer_settings):
"""The command should raise an exception if the indexer is not configured"""
indexer_settings.SEARCH_INDEXER_CLASS = None
with pytest.raises(CommandError) as err:
call_command("index")
assert str(err.value) == "The indexer is not enabled or properly configured."

View File

@@ -24,30 +24,3 @@ def mock_user_teams():
"core.models.User.teams", new_callable=mock.PropertyMock
) as mock_teams:
yield mock_teams
@pytest.fixture(name="indexer_settings")
def indexer_settings_fixture(settings):
"""
Setup valid settings for the document indexer. Clear the indexer cache.
"""
# pylint: disable-next=import-outside-toplevel
from core.services.search_indexers import ( # noqa: PLC0415
get_document_indexer,
)
get_document_indexer.cache_clear()
settings.SEARCH_INDEXER_CLASS = "core.services.search_indexers.SearchIndexer"
settings.SEARCH_INDEXER_SECRET = "ThisIsAKeyForTest"
settings.SEARCH_INDEXER_URL = "http://localhost:8081/api/v1.0/documents/index/"
settings.SEARCH_INDEXER_QUERY_URL = (
"http://localhost:8081/api/v1.0/documents/search/"
)
settings.SEARCH_INDEXER_COUNTDOWN = 1
yield settings
# clear cache to prevent issues with other tests
get_document_indexer.cache_clear()

View File

@@ -1,425 +0,0 @@
"""
Tests for Documents API endpoint in impress's core app: list
"""
import random
from json import loads as json_loads
from django.test import RequestFactory
import pytest
import responses
from faker import Faker
from rest_framework.test import APIClient
from core import factories, models
from core.services.search_indexers import get_document_indexer
fake = Faker()
pytestmark = pytest.mark.django_db
def build_search_url(**kwargs):
"""Build absolute uri for search endpoint with ORDERED query arguments"""
return (
RequestFactory()
.get("/api/v1.0/documents/search/", dict(sorted(kwargs.items())))
.build_absolute_uri()
)
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
@responses.activate
def test_api_documents_search_anonymous(reach, role, indexer_settings):
"""
Anonymous users should not be allowed to search documents whatever the
link reach and link role
"""
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
factories.DocumentFactory(link_reach=reach, link_role=role)
# Find response
responses.add(
responses.POST,
"http://find/api/v1.0/search",
json=[],
status=200,
)
response = APIClient().get("/api/v1.0/documents/search/", data={"q": "alpha"})
assert response.status_code == 200
assert response.json() == {
"count": 0,
"next": None,
"previous": None,
"results": [],
}
def test_api_documents_search_endpoint_is_none(indexer_settings):
"""
Missing SEARCH_INDEXER_QUERY_URL, so the indexer is not properly configured.
Should fallback on title filter
"""
indexer_settings.SEARCH_INDEXER_QUERY_URL = None
assert get_document_indexer() is None
user = factories.UserFactory()
document = factories.DocumentFactory(title="alpha")
access = factories.UserDocumentAccessFactory(document=document, user=user)
client = APIClient()
client.force_login(user)
response = client.get("/api/v1.0/documents/search/", data={"q": "alpha"})
assert response.status_code == 200
content = response.json()
results = content.pop("results")
assert content == {
"count": 1,
"next": None,
"previous": None,
}
assert len(results) == 1
assert results[0] == {
"id": str(document.id),
"abilities": document.get_abilities(user),
"ancestors_link_reach": None,
"ancestors_link_role": None,
"computed_link_reach": document.computed_link_reach,
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 1,
"excerpt": document.excerpt,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 1,
"nb_accesses_direct": 1,
"numchild": 0,
"path": document.path,
"title": document.title,
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
"deleted_at": None,
"user_role": access.role,
}
@responses.activate
def test_api_documents_search_invalid_params(indexer_settings):
"""Validate the format of documents as returned by the search view."""
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
response = client.get("/api/v1.0/documents/search/")
assert response.status_code == 400
assert response.json() == {"q": ["This field is required."]}
response = client.get("/api/v1.0/documents/search/", data={"q": " "})
assert response.status_code == 400
assert response.json() == {"q": ["This field may not be blank."]}
response = client.get(
"/api/v1.0/documents/search/", data={"q": "any", "page": "NaN"}
)
assert response.status_code == 400
assert response.json() == {"page": ["A valid integer is required."]}
@responses.activate
def test_api_documents_search_format(indexer_settings):
"""Validate the format of documents as returned by the search view."""
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
assert get_document_indexer() is not None
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
user_a, user_b, user_c = factories.UserFactory.create_batch(3)
document = factories.DocumentFactory(
title="alpha",
users=(user_a, user_c),
link_traces=(user, user_b),
)
access = factories.UserDocumentAccessFactory(document=document, user=user)
# Find response
responses.add(
responses.POST,
"http://find/api/v1.0/search",
json=[
{"_id": str(document.pk)},
],
status=200,
)
response = client.get("/api/v1.0/documents/search/", data={"q": "alpha"})
assert response.status_code == 200
content = response.json()
results = content.pop("results")
assert content == {
"count": 1,
"next": None,
"previous": None,
}
assert len(results) == 1
assert results[0] == {
"id": str(document.id),
"abilities": document.get_abilities(user),
"ancestors_link_reach": None,
"ancestors_link_role": None,
"computed_link_reach": document.computed_link_reach,
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 1,
"excerpt": document.excerpt,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 3,
"nb_accesses_direct": 3,
"numchild": 0,
"path": document.path,
"title": document.title,
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
"deleted_at": None,
"user_role": access.role,
}
@responses.activate
@pytest.mark.parametrize(
"pagination, status, expected",
(
(
{"page": 1, "page_size": 10},
200,
{
"count": 10,
"previous": None,
"next": None,
"range": (0, None),
},
),
(
{},
200,
{
"count": 10,
"previous": None,
"next": None,
"range": (0, None),
"api_page_size": 21, # default page_size is 20
},
),
(
{"page": 2, "page_size": 10},
404,
{},
),
(
{"page": 1, "page_size": 5},
200,
{
"count": 10,
"previous": None,
"next": {"page": 2, "page_size": 5},
"range": (0, 5),
},
),
(
{"page": 2, "page_size": 5},
200,
{
"count": 10,
"previous": {"page_size": 5},
"next": None,
"range": (5, None),
},
),
({"page": 3, "page_size": 5}, 404, {}),
),
)
def test_api_documents_search_pagination(
indexer_settings, pagination, status, expected
):
"""Documents should be ordered by descending "score" by default"""
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
assert get_document_indexer() is not None
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
docs = factories.DocumentFactory.create_batch(10, title="alpha", users=[user])
docs_by_uuid = {str(doc.pk): doc for doc in docs}
api_results = [{"_id": id} for id in docs_by_uuid.keys()]
# reorder randomly to simulate score ordering
random.shuffle(api_results)
# Find response
# pylint: disable-next=assignment-from-none
api_search = responses.add(
responses.POST,
"http://find/api/v1.0/search",
json=api_results,
status=200,
)
response = client.get(
"/api/v1.0/documents/search/",
data={
"q": "alpha",
**pagination,
},
)
assert response.status_code == status
if response.status_code < 300:
previous_url = (
build_search_url(q="alpha", **expected["previous"])
if expected["previous"]
else None
)
next_url = (
build_search_url(q="alpha", **expected["next"])
if expected["next"]
else None
)
start, end = expected["range"]
content = response.json()
assert content["count"] == expected["count"]
assert content["previous"] == previous_url
assert content["next"] == next_url
results = content.pop("results")
# The find api results ordering by score is kept
assert [r["id"] for r in results] == [r["_id"] for r in api_results[start:end]]
# Check the query parameters.
assert api_search.call_count == 1
assert api_search.calls[0].response.status_code == 200
assert json_loads(api_search.calls[0].request.body) == {
"q": "alpha",
"visited": [],
"services": ["docs"],
"nb_results": 50,
"order_by": "updated_at",
"order_direction": "desc",
}
@responses.activate
@pytest.mark.parametrize(
"pagination, status, expected",
(
(
{"page": 1, "page_size": 10},
200,
{"count": 10, "previous": None, "next": None, "range": (0, None)},
),
(
{},
200,
{"count": 10, "previous": None, "next": None, "range": (0, None)},
),
(
{"page": 2, "page_size": 10},
404,
{},
),
(
{"page": 1, "page_size": 5},
200,
{
"count": 10,
"previous": None,
"next": {"page": 2, "page_size": 5},
"range": (0, 5),
},
),
(
{"page": 2, "page_size": 5},
200,
{
"count": 10,
"previous": {"page_size": 5},
"next": None,
"range": (5, None),
},
),
({"page": 3, "page_size": 5}, 404, {}),
),
)
def test_api_documents_search_pagination_endpoint_is_none(
indexer_settings, pagination, status, expected
):
"""Documents should be ordered by descending "-updated_at" by default"""
indexer_settings.SEARCH_INDEXER_QUERY_URL = None
assert get_document_indexer() is None
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(10, title="alpha", users=[user])
response = client.get(
"/api/v1.0/documents/search/",
data={
"q": "alpha",
**pagination,
},
)
assert response.status_code == status
if response.status_code < 300:
previous_url = (
build_search_url(q="alpha", **expected["previous"])
if expected["previous"]
else None
)
next_url = (
build_search_url(q="alpha", **expected["next"])
if expected["next"]
else None
)
queryset = models.Document.objects.order_by("-updated_at")
start, end = expected["range"]
expected_results = [str(d.pk) for d in queryset[start:end]]
content = response.json()
assert content["count"] == expected["count"]
assert content["previous"] == previous_url
assert content["next"] == next_url
results = content.pop("results")
assert [r["id"] for r in results] == expected_results

View File

@@ -1,107 +0,0 @@
"""
Test DocumentThrottle for regular throttling and y-provider bypass.
"""
import pytest
from rest_framework.test import APIClient
from core import factories
pytestmark = pytest.mark.django_db
def test_api_throttling_document_throttle_regular_requests(settings):
"""Test that regular requests are throttled normally."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = "3/minute"
settings.Y_PROVIDER_API_KEY = "test-y-provider-key"
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user)
# Make 3 requests without the y-provider key
for _i in range(3):
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
)
assert response.status_code == 200
# 4th request should be throttled
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
)
assert response.status_code == 429
# A request with the y-provider key should NOT be throttled
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
HTTP_X_Y_PROVIDER_KEY="test-y-provider-key",
)
assert response.status_code == 200
# Restore original rate
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = current_rate
def test_api_throttling_document_throttle_y_provider_exempted(settings):
"""Test that y-provider requests are exempted from throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = "3/minute"
settings.Y_PROVIDER_API_KEY = "test-y-provider-key"
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user)
# Make many requests with the y-provider API key
for _i in range(10):
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
HTTP_X_Y_PROVIDER_KEY="test-y-provider-key",
)
assert response.status_code == 200
# Restore original rate
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = current_rate
def test_api_throttling_document_throttle_invalid_token(settings):
"""Test that requests with invalid tokens are throttled."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = "3/minute"
settings.Y_PROVIDER_API_KEY = "test-y-provider-key"
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user)
# Make 3 requests with an invalid token
for _i in range(3):
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
HTTP_X_Y_PROVIDER_KEY="invalid-token",
)
assert response.status_code == 200
# 4th request should be throttled
response = client.get(
f"/api/v1.0/documents/{document.id!s}/",
HTTP_X_Y_PROVIDER_KEY="invalid-token",
)
assert response.status_code == 429
# Restore original rate
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["document"] = current_rate

View File

@@ -76,131 +76,6 @@ def test_api_users_list_query_email():
assert user_ids == []
def test_api_users_list_query_email_with_internationalized_domain_names():
"""
Authenticated users should be able to list users and filter by email.
It should work even if the email address contains an internationalized domain name.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
jean = factories.UserFactory(email="jean.martin@éducation.fr")
marie = factories.UserFactory(email="marie.durand@education.fr")
kurokawa = factories.UserFactory(email="contact@黒川.日本")
response = client.get("/api/v1.0/users/?q=jean.martin@education.fr")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(jean.id)]
response = client.get("/api/v1.0/users/?q=jean.martin@éducation.fr")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(jean.id)]
response = client.get("/api/v1.0/users/?q=marie.durand@education.fr")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(marie.id)]
response = client.get("/api/v1.0/users/?q=marie.durand@éducation.fr")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(marie.id)]
response = client.get("/api/v1.0/users/?q=contact@黒川.日本")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(kurokawa.id)]
def test_api_users_list_query_full_name():
"""
Authenticated users should be able to list users and filter by full name.
Only results with a Trigram similarity greater than 0.2 with the query should be returned.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
dave = factories.UserFactory(email="contact@work.com", full_name="David Bowman")
response = client.get(
"/api/v1.0/users/?q=David",
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get("/api/v1.0/users/?q=Bowman")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get("/api/v1.0/users/?q=bowman")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get("/api/v1.0/users/?q=BOWMAN")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get("/api/v1.0/users/?q=BoWmAn")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get("/api/v1.0/users/?q=Bovin")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == []
def test_api_users_list_query_accented_full_name():
"""
Authenticated users should be able to list users and filter by full name with accents.
Only results with a Trigram similarity greater than 0.2 with the query should be returned.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
fred = factories.UserFactory(
email="contact@work.com", full_name="Frédérique Lefèvre"
)
response = client.get("/api/v1.0/users/?q=Frédérique")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(fred.id)]
response = client.get("/api/v1.0/users/?q=Frederique")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(fred.id)]
response = client.get("/api/v1.0/users/?q=Lefèvre")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(fred.id)]
response = client.get("/api/v1.0/users/?q=Lefevre")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(fred.id)]
response = client.get("/api/v1.0/users/?q=François Lorfebvre")
assert response.status_code == 200
users = [user["full_name"] for user in response.json()]
assert users == []
def test_api_users_list_limit(settings):
"""
Authenticated users should be able to list users and the number of results

View File

@@ -1,441 +0,0 @@
"""
Unit tests for the Document model
"""
# pylint: disable=too-many-lines
from operator import itemgetter
from unittest import mock
from django.core.cache import cache
from django.db import transaction
import pytest
from core import factories, models
from core.services.search_indexers import SearchIndexer
pytestmark = pytest.mark.django_db
def reset_batch_indexer_throttle():
"""Reset throttle flag"""
cache.delete("document-batch-indexer-throttle")
@pytest.fixture(autouse=True)
def reset_throttle():
"""Reset throttle flag before each test"""
reset_batch_indexer_throttle()
yield
reset_batch_indexer_throttle()
@mock.patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer(mock_push):
"""Test indexation task on document creation"""
with transaction.atomic():
doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3)
accesses = {}
data = [call.args[0] for call in mock_push.call_args_list]
indexer = SearchIndexer()
assert len(data) == 1
# One call
assert sorted(data[0], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc1, accesses),
indexer.serialize_document(doc2, accesses),
indexer.serialize_document(doc3, accesses),
],
key=itemgetter("id"),
)
# The throttle counters should be reset
assert cache.get("document-batch-indexer-throttle") == 1
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer_no_batches(indexer_settings):
"""Test indexation task on doculment creation, no throttle"""
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
with mock.patch.object(SearchIndexer, "push") as mock_push:
with transaction.atomic():
doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3)
accesses = {}
data = [call.args[0] for call in mock_push.call_args_list]
indexer = SearchIndexer()
# 3 calls
assert len(data) == 3
# one document per call
assert [len(d) for d in data] == [1] * 3
# all documents are indexed
assert sorted([d[0] for d in data], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc1, accesses),
indexer.serialize_document(doc2, accesses),
indexer.serialize_document(doc3, accesses),
],
key=itemgetter("id"),
)
# The throttle counters should be reset
assert cache.get("file-batch-indexer-throttle") is None
@mock.patch.object(SearchIndexer, "push")
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer_not_configured(mock_push, indexer_settings):
"""Task should not start an indexation when disabled"""
indexer_settings.SEARCH_INDEXER_CLASS = None
user = factories.UserFactory()
with transaction.atomic():
doc = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=doc, user=user)
assert mock_push.assert_not_called
@mock.patch.object(SearchIndexer, "push")
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer_wrongly_configured(
mock_push, indexer_settings
):
"""Task should not start an indexation when disabled"""
indexer_settings.SEARCH_INDEXER_URL = None
user = factories.UserFactory()
with transaction.atomic():
doc = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=doc, user=user)
assert mock_push.assert_not_called
@mock.patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer_with_accesses(mock_push):
"""Test indexation task on document creation"""
user = factories.UserFactory()
with transaction.atomic():
doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3)
factories.UserDocumentAccessFactory(document=doc1, user=user)
factories.UserDocumentAccessFactory(document=doc2, user=user)
factories.UserDocumentAccessFactory(document=doc3, user=user)
accesses = {
str(doc1.path): {"users": [user.sub]},
str(doc2.path): {"users": [user.sub]},
str(doc3.path): {"users": [user.sub]},
}
data = [call.args[0] for call in mock_push.call_args_list]
indexer = SearchIndexer()
assert len(data) == 1
assert sorted(data[0], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc1, accesses),
indexer.serialize_document(doc2, accesses),
indexer.serialize_document(doc3, accesses),
],
key=itemgetter("id"),
)
@mock.patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer_deleted(mock_push):
"""Indexation task on deleted or ancestor_deleted documents"""
user = factories.UserFactory()
with transaction.atomic():
doc = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED
)
main_doc = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED
)
child_doc = factories.DocumentFactory(
parent=main_doc,
link_reach=models.LinkReachChoices.AUTHENTICATED,
)
factories.UserDocumentAccessFactory(document=doc, user=user)
factories.UserDocumentAccessFactory(document=main_doc, user=user)
factories.UserDocumentAccessFactory(document=child_doc, user=user)
# Manually reset the throttle flag here or the next indexation will be ignored for 1 second
reset_batch_indexer_throttle()
with transaction.atomic():
main_doc_deleted = models.Document.objects.get(pk=main_doc.pk)
main_doc_deleted.soft_delete()
child_doc_deleted = models.Document.objects.get(pk=child_doc.pk)
main_doc_deleted.refresh_from_db()
child_doc_deleted.refresh_from_db()
assert main_doc_deleted.deleted_at is not None
assert child_doc_deleted.ancestors_deleted_at is not None
assert child_doc_deleted.deleted_at is None
assert child_doc_deleted.ancestors_deleted_at is not None
accesses = {
str(doc.path): {"users": [user.sub]},
str(main_doc_deleted.path): {"users": [user.sub]},
str(child_doc_deleted.path): {"users": [user.sub]},
}
data = [call.args[0] for call in mock_push.call_args_list]
indexer = SearchIndexer()
assert len(data) == 2
# First indexation on document creation
assert sorted(data[0], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc, accesses),
indexer.serialize_document(main_doc, accesses),
indexer.serialize_document(child_doc, accesses),
],
key=itemgetter("id"),
)
# Even deleted items are re-indexed : only update their status in the future
assert sorted(data[1], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(main_doc_deleted, accesses), # soft_delete()
indexer.serialize_document(child_doc_deleted, accesses),
],
key=itemgetter("id"),
)
@pytest.mark.django_db(transaction=True)
@pytest.mark.usefixtures("indexer_settings")
def test_models_documents_indexer_hard_deleted():
"""Indexation task on hard deleted document"""
user = factories.UserFactory()
with transaction.atomic():
doc = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED
)
factories.UserDocumentAccessFactory(document=doc, user=user)
# Call task on deleted document.
with mock.patch.object(SearchIndexer, "push") as mock_push:
doc.delete()
# Hard delete document are not re-indexed.
assert mock_push.assert_not_called
@mock.patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
@pytest.mark.django_db(transaction=True)
def test_models_documents_post_save_indexer_restored(mock_push):
"""Restart indexation task on restored documents"""
user = factories.UserFactory()
with transaction.atomic():
doc = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED
)
doc_deleted = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED
)
doc_ancestor_deleted = factories.DocumentFactory(
parent=doc_deleted,
link_reach=models.LinkReachChoices.AUTHENTICATED,
)
factories.UserDocumentAccessFactory(document=doc, user=user)
factories.UserDocumentAccessFactory(document=doc_deleted, user=user)
factories.UserDocumentAccessFactory(document=doc_ancestor_deleted, user=user)
doc_deleted.soft_delete()
doc_deleted.refresh_from_db()
doc_ancestor_deleted.refresh_from_db()
assert doc_deleted.deleted_at is not None
assert doc_deleted.ancestors_deleted_at is not None
assert doc_ancestor_deleted.deleted_at is None
assert doc_ancestor_deleted.ancestors_deleted_at is not None
# Manually reset the throttle flag here or the next indexation will be ignored for 1 second
reset_batch_indexer_throttle()
with transaction.atomic():
doc_restored = models.Document.objects.get(pk=doc_deleted.pk)
doc_restored.restore()
doc_ancestor_restored = models.Document.objects.get(pk=doc_ancestor_deleted.pk)
assert doc_restored.deleted_at is None
assert doc_restored.ancestors_deleted_at is None
assert doc_ancestor_restored.deleted_at is None
assert doc_ancestor_restored.ancestors_deleted_at is None
accesses = {
str(doc.path): {"users": [user.sub]},
str(doc_deleted.path): {"users": [user.sub]},
str(doc_ancestor_deleted.path): {"users": [user.sub]},
}
data = [call.args[0] for call in mock_push.call_args_list]
indexer = SearchIndexer()
# All docs are re-indexed
assert len(data) == 2
# First indexation on items creation & soft delete (in the same transaction)
assert sorted(data[0], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc, accesses),
indexer.serialize_document(doc_deleted, accesses),
indexer.serialize_document(doc_ancestor_deleted, accesses),
],
key=itemgetter("id"),
)
# Restored items are re-indexed : only update their status in the future
assert sorted(data[1], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(doc_restored, accesses), # restore()
indexer.serialize_document(doc_ancestor_restored, accesses),
],
key=itemgetter("id"),
)
@pytest.mark.django_db(transaction=True)
@pytest.mark.usefixtures("indexer_settings")
def test_models_documents_post_save_indexer_throttle():
"""Test indexation task skipping on document update"""
indexer = SearchIndexer()
user = factories.UserFactory()
with mock.patch.object(SearchIndexer, "push"):
with transaction.atomic():
docs = factories.DocumentFactory.create_batch(5, users=(user,))
accesses = {str(item.path): {"users": [user.sub]} for item in docs}
with mock.patch.object(SearchIndexer, "push") as mock_push:
# Simulate 1 running task
cache.set("document-batch-indexer-throttle", 1)
# save doc to trigger the indexer, but nothing should be done since
# the flag is up
with transaction.atomic():
docs[0].save()
docs[2].save()
docs[3].save()
assert [call.args[0] for call in mock_push.call_args_list] == []
with mock.patch.object(SearchIndexer, "push") as mock_push:
# No waiting task
cache.delete("document-batch-indexer-throttle")
with transaction.atomic():
docs[0].save()
docs[2].save()
docs[3].save()
data = [call.args[0] for call in mock_push.call_args_list]
# One call
assert len(data) == 1
assert sorted(data[0], key=itemgetter("id")) == sorted(
[
indexer.serialize_document(docs[0], accesses),
indexer.serialize_document(docs[2], accesses),
indexer.serialize_document(docs[3], accesses),
],
key=itemgetter("id"),
)
@pytest.mark.django_db(transaction=True)
@pytest.mark.usefixtures("indexer_settings")
def test_models_documents_access_post_save_indexer():
"""Test indexation task on DocumentAccess update"""
users = factories.UserFactory.create_batch(3)
with mock.patch.object(SearchIndexer, "push"):
with transaction.atomic():
doc = factories.DocumentFactory(users=users)
doc_accesses = models.DocumentAccess.objects.filter(document=doc).order_by(
"user__sub"
)
reset_batch_indexer_throttle()
with mock.patch.object(SearchIndexer, "push") as mock_push:
with transaction.atomic():
for doc_access in doc_accesses:
doc_access.save()
data = [call.args[0] for call in mock_push.call_args_list]
# One call
assert len(data) == 1
assert [d["id"] for d in data[0]] == [str(doc.pk)]
@pytest.mark.django_db(transaction=True)
def test_models_items_access_post_save_indexer_no_throttle(indexer_settings):
"""Test indexation task on ItemAccess update, no throttle"""
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
users = factories.UserFactory.create_batch(3)
with transaction.atomic():
doc = factories.DocumentFactory(users=users)
doc_accesses = models.DocumentAccess.objects.filter(document=doc).order_by(
"user__sub"
)
reset_batch_indexer_throttle()
with mock.patch.object(SearchIndexer, "push") as mock_push:
with transaction.atomic():
for doc_access in doc_accesses:
doc_access.save()
data = [call.args[0] for call in mock_push.call_args_list]
# 3 calls
assert len(data) == 3
# one document per call
assert [len(d) for d in data] == [1] * 3
# the same document is indexed 3 times
assert [d[0]["id"] for d in data] == [str(doc.pk)] * 3

View File

@@ -1,635 +0,0 @@
"""Tests for Documents search indexers"""
from functools import partial
from json import dumps as json_dumps
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
import pytest
import responses
from requests import HTTPError
from core import factories, models, utils
from core.services.search_indexers import (
BaseDocumentIndexer,
SearchIndexer,
get_document_indexer,
get_visited_document_ids_of,
)
pytestmark = pytest.mark.django_db
class FakeDocumentIndexer(BaseDocumentIndexer):
"""Fake indexer for test purpose"""
def serialize_document(self, document, accesses):
return {}
def push(self, data):
pass
def search_query(self, data, token):
return {}
def test_services_search_indexer_class_invalid(indexer_settings):
"""
Should raise RuntimeError if SEARCH_INDEXER_CLASS cannot be imported.
"""
indexer_settings.SEARCH_INDEXER_CLASS = "unknown.Unknown"
assert get_document_indexer() is None
def test_services_search_indexer_class(indexer_settings):
"""
Import indexer class defined in setting SEARCH_INDEXER_CLASS.
"""
indexer_settings.SEARCH_INDEXER_CLASS = (
"core.tests.test_services_search_indexers.FakeDocumentIndexer"
)
assert isinstance(
get_document_indexer(),
import_string("core.tests.test_services_search_indexers.FakeDocumentIndexer"),
)
def test_services_search_indexer_is_configured(indexer_settings):
"""
Should return true only when the indexer class and other configuration settings
are valid.
"""
indexer_settings.SEARCH_INDEXER_CLASS = None
# None
get_document_indexer.cache_clear()
assert not get_document_indexer()
# Empty
indexer_settings.SEARCH_INDEXER_CLASS = ""
get_document_indexer.cache_clear()
assert not get_document_indexer()
# Valid class
indexer_settings.SEARCH_INDEXER_CLASS = (
"core.services.search_indexers.SearchIndexer"
)
get_document_indexer.cache_clear()
assert get_document_indexer() is not None
indexer_settings.SEARCH_INDEXER_URL = ""
# Invalid url
get_document_indexer.cache_clear()
assert not get_document_indexer()
def test_services_search_indexer_url_is_none(indexer_settings):
"""
Indexer should raise RuntimeError if SEARCH_INDEXER_URL is None or empty.
"""
indexer_settings.SEARCH_INDEXER_URL = None
with pytest.raises(ImproperlyConfigured) as exc_info:
SearchIndexer()
assert "SEARCH_INDEXER_URL must be set in Django settings." in str(exc_info.value)
def test_services_search_indexer_url_is_empty(indexer_settings):
"""
Indexer should raise RuntimeError if SEARCH_INDEXER_URL is empty string.
"""
indexer_settings.SEARCH_INDEXER_URL = ""
with pytest.raises(ImproperlyConfigured) as exc_info:
SearchIndexer()
assert "SEARCH_INDEXER_URL must be set in Django settings." in str(exc_info.value)
def test_services_search_indexer_secret_is_none(indexer_settings):
"""
Indexer should raise RuntimeError if SEARCH_INDEXER_SECRET is None.
"""
indexer_settings.SEARCH_INDEXER_SECRET = None
with pytest.raises(ImproperlyConfigured) as exc_info:
SearchIndexer()
assert "SEARCH_INDEXER_SECRET must be set in Django settings." in str(
exc_info.value
)
def test_services_search_indexer_secret_is_empty(indexer_settings):
"""
Indexer should raise RuntimeError if SEARCH_INDEXER_SECRET is empty string.
"""
indexer_settings.SEARCH_INDEXER_SECRET = ""
with pytest.raises(ImproperlyConfigured) as exc_info:
SearchIndexer()
assert "SEARCH_INDEXER_SECRET must be set in Django settings." in str(
exc_info.value
)
def test_services_search_endpoint_is_none(indexer_settings):
"""
Indexer should raise RuntimeError if SEARCH_INDEXER_QUERY_URL is None.
"""
indexer_settings.SEARCH_INDEXER_QUERY_URL = None
with pytest.raises(ImproperlyConfigured) as exc_info:
SearchIndexer()
assert "SEARCH_INDEXER_QUERY_URL must be set in Django settings." in str(
exc_info.value
)
def test_services_search_endpoint_is_empty(indexer_settings):
"""
Indexer should raise RuntimeError if SEARCH_INDEXER_QUERY_URL is empty.
"""
indexer_settings.SEARCH_INDEXER_QUERY_URL = ""
with pytest.raises(ImproperlyConfigured) as exc_info:
SearchIndexer()
assert "SEARCH_INDEXER_QUERY_URL must be set in Django settings." in str(
exc_info.value
)
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_serialize_document_returns_expected_json():
"""
It should serialize documents with correct metadata and access control.
"""
user_a, user_b = factories.UserFactory.create_batch(2)
document = factories.DocumentFactory()
factories.DocumentFactory(parent=document)
factories.UserDocumentAccessFactory(document=document, user=user_a)
factories.UserDocumentAccessFactory(document=document, user=user_b)
factories.TeamDocumentAccessFactory(document=document, team="team1")
factories.TeamDocumentAccessFactory(document=document, team="team2")
accesses = {
document.path: {
"users": {str(user_a.sub), str(user_b.sub)},
"teams": {"team1", "team2"},
}
}
indexer = SearchIndexer()
result = indexer.serialize_document(document, accesses)
assert set(result.pop("users")) == {str(user_a.sub), str(user_b.sub)}
assert set(result.pop("groups")) == {"team1", "team2"}
assert result == {
"id": str(document.id),
"title": document.title,
"depth": 1,
"path": document.path,
"numchild": 1,
"content": utils.base64_yjs_to_text(document.content),
"created_at": document.created_at.isoformat(),
"updated_at": document.updated_at.isoformat(),
"reach": document.link_reach,
"size": 13,
"is_active": True,
}
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_serialize_document_deleted():
"""Deleted documents are marked as just in the serialized json."""
parent = factories.DocumentFactory()
document = factories.DocumentFactory(parent=parent)
parent.soft_delete()
document.refresh_from_db()
indexer = SearchIndexer()
result = indexer.serialize_document(document, {})
assert result["is_active"] is False
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_serialize_document_empty():
"""Empty documents returns empty content in the serialized json."""
document = factories.DocumentFactory(content="", title=None)
indexer = SearchIndexer()
result = indexer.serialize_document(document, {})
assert result["content"] == ""
assert result["title"] == ""
@responses.activate
def test_services_search_indexers_index_errors(indexer_settings):
"""
Documents indexing response handling on Find API HTTP errors.
"""
factories.DocumentFactory()
indexer_settings.SEARCH_INDEXER_URL = "http://app-find/api/v1.0/documents/index/"
responses.add(
responses.POST,
"http://app-find/api/v1.0/documents/index/",
status=401,
body=json_dumps({"message": "Authentication failed."}),
)
with pytest.raises(HTTPError):
SearchIndexer().index()
@patch.object(SearchIndexer, "push")
def test_services_search_indexers_batches_pass_only_batch_accesses(
mock_push, indexer_settings
):
"""
Documents indexing should be processed in batches,
and only the access data relevant to each batch should be used.
"""
indexer_settings.SEARCH_INDEXER_BATCH_SIZE = 2
documents = factories.DocumentFactory.create_batch(5)
# Attach a single user access to each document
expected_user_subs = {}
for document in documents:
access = factories.UserDocumentAccessFactory(document=document)
expected_user_subs[str(document.id)] = str(access.user.sub)
assert SearchIndexer().index() == 5
# Should be 3 batches: 2 + 2 + 1
assert mock_push.call_count == 3
seen_doc_ids = set()
for call in mock_push.call_args_list:
batch = call.args[0]
assert isinstance(batch, list)
for doc_json in batch:
doc_id = doc_json["id"]
seen_doc_ids.add(doc_id)
# Only one user expected per document
assert doc_json["users"] == [expected_user_subs[doc_id]]
assert doc_json["groups"] == []
# Make sure all 5 documents were indexed
assert seen_doc_ids == {str(d.id) for d in documents}
@patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_batch_size_argument(mock_push):
"""
Documents indexing should be processed in batches,
batch_size overrides SEARCH_INDEXER_BATCH_SIZE
"""
documents = factories.DocumentFactory.create_batch(5)
# Attach a single user access to each document
expected_user_subs = {}
for document in documents:
access = factories.UserDocumentAccessFactory(document=document)
expected_user_subs[str(document.id)] = str(access.user.sub)
assert SearchIndexer().index(batch_size=2) == 5
# Should be 3 batches: 2 + 2 + 1
assert mock_push.call_count == 3
seen_doc_ids = set()
for call in mock_push.call_args_list:
batch = call.args[0]
assert isinstance(batch, list)
for doc_json in batch:
doc_id = doc_json["id"]
seen_doc_ids.add(doc_id)
# Only one user expected per document
assert doc_json["users"] == [expected_user_subs[doc_id]]
assert doc_json["groups"] == []
# Make sure all 5 documents were indexed
assert seen_doc_ids == {str(d.id) for d in documents}
@patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_ignore_empty_documents(mock_push):
"""
Documents indexing should be processed in batches,
and only the access data relevant to each batch should be used.
"""
document = factories.DocumentFactory()
factories.DocumentFactory(content="", title="")
empty_title = factories.DocumentFactory(title="")
empty_content = factories.DocumentFactory(content="")
assert SearchIndexer().index() == 3
assert mock_push.call_count == 1
# Make sure only not eempty documents are indexed
results = {doc["id"] for doc in mock_push.call_args[0][0]}
assert results == {
str(d.id)
for d in (
document,
empty_content,
empty_title,
)
}
@patch.object(SearchIndexer, "push")
def test_services_search_indexers_skip_empty_batches(mock_push, indexer_settings):
"""
Documents indexing batch can be empty if all the docs are empty.
"""
indexer_settings.SEARCH_INDEXER_BATCH_SIZE = 2
document = factories.DocumentFactory()
# Only empty docs
factories.DocumentFactory.create_batch(5, content="", title="")
assert SearchIndexer().index() == 1
assert mock_push.call_count == 1
results = [doc["id"] for doc in mock_push.call_args[0][0]]
assert results == [str(document.id)]
@patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_ancestors_link_reach(mock_push):
"""Document accesses and reach should take into account ancestors link reaches."""
great_grand_parent = factories.DocumentFactory(link_reach="restricted")
grand_parent = factories.DocumentFactory(
parent=great_grand_parent, link_reach="authenticated"
)
parent = factories.DocumentFactory(parent=grand_parent, link_reach="public")
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
assert SearchIndexer().index() == 4
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
assert len(results) == 4
assert results[str(great_grand_parent.id)]["reach"] == "restricted"
assert results[str(grand_parent.id)]["reach"] == "authenticated"
assert results[str(parent.id)]["reach"] == "public"
assert results[str(document.id)]["reach"] == "public"
@patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_ancestors_users(mock_push):
"""Document accesses and reach should include users from ancestors."""
user_gp, user_p, user_d = factories.UserFactory.create_batch(3)
grand_parent = factories.DocumentFactory(users=[user_gp])
parent = factories.DocumentFactory(parent=grand_parent, users=[user_p])
document = factories.DocumentFactory(parent=parent, users=[user_d])
assert SearchIndexer().index() == 3
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
assert len(results) == 3
assert results[str(grand_parent.id)]["users"] == [str(user_gp.sub)]
assert set(results[str(parent.id)]["users"]) == {str(user_gp.sub), str(user_p.sub)}
assert set(results[str(document.id)]["users"]) == {
str(user_gp.sub),
str(user_p.sub),
str(user_d.sub),
}
@patch.object(SearchIndexer, "push")
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_ancestors_teams(mock_push):
"""Document accesses and reach should include teams from ancestors."""
grand_parent = factories.DocumentFactory(teams=["team_gp"])
parent = factories.DocumentFactory(parent=grand_parent, teams=["team_p"])
document = factories.DocumentFactory(parent=parent, teams=["team_d"])
assert SearchIndexer().index() == 3
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
assert len(results) == 3
assert results[str(grand_parent.id)]["groups"] == ["team_gp"]
assert set(results[str(parent.id)]["groups"]) == {"team_gp", "team_p"}
assert set(results[str(document.id)]["groups"]) == {"team_gp", "team_p", "team_d"}
@patch("requests.post")
def test_push_uses_correct_url_and_data(mock_post, indexer_settings):
"""
push() should call requests.post with the correct URL from settings
the timeout set to 10 seconds and the data as JSON.
"""
indexer_settings.SEARCH_INDEXER_URL = "http://example.com/index"
indexer = SearchIndexer()
sample_data = [{"id": "123", "title": "Test"}]
mock_response = mock_post.return_value
mock_response.raise_for_status.return_value = None # No error
indexer.push(sample_data)
mock_post.assert_called_once()
args, kwargs = mock_post.call_args
assert args[0] == indexer_settings.SEARCH_INDEXER_URL
assert kwargs.get("json") == sample_data
assert kwargs.get("timeout") == 10
def test_get_visited_document_ids_of():
"""
get_visited_document_ids_of() returns the ids of the documents viewed
by the user BUT without specific access configuration (like public ones)
"""
user = factories.UserFactory()
other = factories.UserFactory()
anonymous = AnonymousUser()
queryset = models.Document.objects.all()
assert not get_visited_document_ids_of(queryset, anonymous)
assert not get_visited_document_ids_of(queryset, user)
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link(document=doc1)
create_link(document=doc2)
# The third document is not visited
assert sorted(get_visited_document_ids_of(queryset, user)) == sorted(
[str(doc1.pk), str(doc2.pk)]
)
factories.UserDocumentAccessFactory(user=other, document=doc1)
factories.UserDocumentAccessFactory(user=user, document=doc2)
# The second document have an access for the user
assert get_visited_document_ids_of(queryset, user) == [str(doc1.pk)]
@pytest.mark.usefixtures("indexer_settings")
def test_get_visited_document_ids_of_deleted():
"""
get_visited_document_ids_of() returns the ids of the documents viewed
by the user if they are not deleted.
"""
user = factories.UserFactory()
anonymous = AnonymousUser()
queryset = models.Document.objects.all()
assert not get_visited_document_ids_of(queryset, anonymous)
assert not get_visited_document_ids_of(queryset, user)
doc = factories.DocumentFactory()
doc_deleted = factories.DocumentFactory()
doc_ancestor_deleted = factories.DocumentFactory(parent=doc_deleted)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link(document=doc)
create_link(document=doc_deleted)
create_link(document=doc_ancestor_deleted)
# The all documents are visited
assert sorted(get_visited_document_ids_of(queryset, user)) == sorted(
[str(doc.pk), str(doc_deleted.pk), str(doc_ancestor_deleted.pk)]
)
doc_deleted.soft_delete()
# Only the first document is not deleted
assert get_visited_document_ids_of(queryset, user) == [str(doc.pk)]
@responses.activate
def test_services_search_indexers_search_errors(indexer_settings):
"""
Documents indexing response handling on Find API HTTP errors.
"""
factories.DocumentFactory()
indexer_settings.SEARCH_INDEXER_QUERY_URL = (
"http://app-find/api/v1.0/documents/search/"
)
responses.add(
responses.POST,
"http://app-find/api/v1.0/documents/search/",
status=401,
body=json_dumps({"message": "Authentication failed."}),
)
with pytest.raises(HTTPError):
SearchIndexer().search("alpha", token="mytoken")
@patch("requests.post")
def test_services_search_indexers_search(mock_post, indexer_settings):
"""
search() should call requests.post to SEARCH_INDEXER_QUERY_URL with the
document ids from linktraces.
"""
user = factories.UserFactory()
indexer = SearchIndexer()
mock_response = mock_post.return_value
mock_response.raise_for_status.return_value = None # No error
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link(document=doc1)
create_link(document=doc2)
visited = get_visited_document_ids_of(models.Document.objects.all(), user)
indexer.search("alpha", visited=visited, token="mytoken")
args, kwargs = mock_post.call_args
assert args[0] == indexer_settings.SEARCH_INDEXER_QUERY_URL
query_data = kwargs.get("json")
assert query_data["q"] == "alpha"
assert sorted(query_data["visited"]) == sorted([str(doc1.pk), str(doc2.pk)])
assert query_data["services"] == ["docs"]
assert query_data["nb_results"] == 50
assert query_data["order_by"] == "updated_at"
assert query_data["order_direction"] == "desc"
assert kwargs.get("headers") == {"Authorization": "Bearer mytoken"}
assert kwargs.get("timeout") == 10
@patch("requests.post")
def test_services_search_indexers_search_nb_results(mock_post, indexer_settings):
"""
Find API call should have nb_results == SEARCH_INDEXER_QUERY_LIMIT
or the given nb_results argument.
"""
indexer_settings.SEARCH_INDEXER_QUERY_LIMIT = 25
user = factories.UserFactory()
indexer = SearchIndexer()
mock_response = mock_post.return_value
mock_response.raise_for_status.return_value = None # No error
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link(document=doc1)
create_link(document=doc2)
visited = get_visited_document_ids_of(models.Document.objects.all(), user)
indexer.search("alpha", visited=visited, token="mytoken")
args, kwargs = mock_post.call_args
assert args[0] == indexer_settings.SEARCH_INDEXER_QUERY_URL
assert kwargs.get("json")["nb_results"] == 25
# The argument overrides the setting value
indexer.search("alpha", visited=visited, token="mytoken", nb_results=109)
args, kwargs = mock_post.call_args
assert args[0] == indexer_settings.SEARCH_INDEXER_QUERY_URL
assert kwargs.get("json")["nb_results"] == 109

View File

@@ -75,28 +75,3 @@ def test_utils_extract_attachments():
base64_string = base64.b64encode(update).decode("utf-8")
# image_key2 is missing the "/media/" part and shouldn't get extracted
assert utils.extract_attachments(base64_string) == [image_key1, image_key3]
def test_utils_get_ancestor_to_descendants_map_single_path():
"""Test ancestor mapping of a single path."""
paths = ["000100020005"]
result = utils.get_ancestor_to_descendants_map(paths, steplen=4)
assert result == {
"0001": {"000100020005"},
"00010002": {"000100020005"},
"000100020005": {"000100020005"},
}
def test_utils_get_ancestor_to_descendants_map_multiple_paths():
"""Test ancestor mapping of multiple paths with shared prefixes."""
paths = ["000100020005", "00010003"]
result = utils.get_ancestor_to_descendants_map(paths, steplen=4)
assert result == {
"0001": {"000100020005", "00010003"},
"00010002": {"000100020005"},
"000100020005": {"000100020005"},
"00010003": {"00010003"},
}

View File

@@ -2,7 +2,6 @@
import base64
import re
from collections import defaultdict
import pycrdt
from bs4 import BeautifulSoup
@@ -10,27 +9,6 @@ from bs4 import BeautifulSoup
from core import enums
def get_ancestor_to_descendants_map(paths, steplen):
"""
Given a list of document paths, return a mapping of ancestor_path -> set of descendant_paths.
Each path is assumed to use materialized path format with fixed-length segments.
Args:
paths (list of str): List of full document paths.
steplen (int): Length of each path segment.
Returns:
dict[str, set[str]]: Mapping from ancestor path to its descendant paths (including itself).
"""
ancestor_map = defaultdict(set)
for path in paths:
for i in range(steplen, len(path) + 1, steplen):
ancestor = path[:i]
ancestor_map[ancestor].add(path)
return ancestor_map
def filter_descendants(paths, root_paths, skip_sorting=False):
"""
Filters paths to keep only those that are descendants of any path in root_paths.

View File

@@ -1,27 +1,23 @@
# ruff: noqa: S311, S106
"""create_demo management command"""
import base64
import logging
import math
import random
import time
from collections import defaultdict
from uuid import uuid4
from django import db
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
import pycrdt
from faker import Faker
from core import models
from demo import defaults
languages = [x for (x, y) in settings.LANGUAGES]
fake = Faker(languages)
fake = Faker()
logger = logging.getLogger("impress.commands.demo.create_demo")
@@ -31,16 +27,6 @@ def random_true_with_probability(probability):
return random.random() < probability
def get_ydoc_for_text(text):
"""Return a ydoc from plain text for demo purposes."""
ydoc = pycrdt.Doc()
paragraph = pycrdt.XmlElement("p", {}, [pycrdt.XmlText(text)])
fragment = pycrdt.XmlFragment([paragraph])
ydoc["document-store"] = fragment
update = ydoc.get_update()
return base64.b64encode(update).decode("utf-8")
class BulkQueue:
"""A utility class to create Django model instances in bulk by just pushing to a queue."""
@@ -62,7 +48,7 @@ class BulkQueue:
self.queue[objects[0]._meta.model.__name__] = [] # noqa: SLF001
def push(self, obj):
"""Add a model instance to queue so that it gets created in bulk."""
"""Add a model instance to queue to that it gets created in bulk."""
objects = self.queue[obj._meta.model.__name__] # noqa: SLF001
objects.append(obj)
if len(objects) > self.BATCH_SIZE:
@@ -141,7 +127,7 @@ def create_demo(stdout):
is_staff=False,
short_name=first_name,
full_name=f"{first_name:s} {random.choice(last_names):s}",
language=random.choice(languages),
language=random.choice(settings.LANGUAGES)[0],
)
)
queue.flush()
@@ -153,19 +139,17 @@ def create_demo(stdout):
# pylint: disable=protected-access
key = models.Document._int2str(i) # noqa: SLF001
padding = models.Document.alphabet[0] * (models.Document.steplen - len(key))
title = fake.sentence(nb_words=4)
document = models.Document(
id=uuid4(),
depth=1,
path=f"{padding}{key}",
creator_id=random.choice(users_ids),
title=title,
link_reach=models.LinkReachChoices.AUTHENTICATED
if random_true_with_probability(0.5)
else random.choice(models.LinkReachChoices.values),
queue.push(
models.Document(
depth=1,
path=f"{padding}{key}",
creator_id=random.choice(users_ids),
title=fake.sentence(nb_words=4),
link_reach=models.LinkReachChoices.AUTHENTICATED
if random_true_with_probability(0.5)
else random.choice(models.LinkReachChoices.values),
)
)
document.save_content(get_ydoc_for_text(f"Content for {title:s}"))
queue.push(document)
queue.flush()
@@ -195,7 +179,8 @@ def create_demo(stdout):
is_superuser=False,
is_active=True,
is_staff=False,
language=dev_user["language"] or random.choice(languages),
language=dev_user["language"]
or random.choice(settings.LANGUAGES)[0],
)
)

View File

@@ -99,31 +99,6 @@ class Base(Configuration):
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Search
SEARCH_INDEXER_CLASS = values.Value(
default=None,
environ_name="SEARCH_INDEXER_CLASS",
environ_prefix=None,
)
SEARCH_INDEXER_BATCH_SIZE = values.IntegerValue(
default=100_000, environ_name="SEARCH_INDEXER_BATCH_SIZE", environ_prefix=None
)
SEARCH_INDEXER_URL = values.Value(
default=None, environ_name="SEARCH_INDEXER_URL", environ_prefix=None
)
SEARCH_INDEXER_COUNTDOWN = values.IntegerValue(
default=1, environ_name="SEARCH_INDEXER_COUNTDOWN", environ_prefix=None
)
SEARCH_INDEXER_SECRET = values.Value(
default=None, environ_name="SEARCH_INDEXER_SECRET", environ_prefix=None
)
SEARCH_INDEXER_QUERY_URL = values.Value(
default=None, environ_name="SEARCH_INDEXER_QUERY_URL", environ_prefix=None
)
SEARCH_INDEXER_QUERY_LIMIT = values.PositiveIntegerValue(
default=50, environ_name="SEARCH_INDEXER_QUERY_LIMIT", environ_prefix=None
)
# Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(DATA_DIR, "static")
@@ -353,7 +328,6 @@ class Base(Configuration):
# OIDC third party
"mozilla_django_oidc",
"lasuite.malware_detection",
"lasuite.marketing",
"csp",
]
@@ -834,30 +808,6 @@ class Base(Configuration):
),
}
# Marketing and communication settings
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
False,
environ_name="SIGNUP_NEW_USER_TO_MARKETING_EMAIL",
environ_prefix=None,
help_text=(
"When enabled, new users are automatically added to mailing list "
"for product updates, marketing communications, and customized emails. "
),
)
LASUITE_MARKETING = {
"BACKEND": values.Value(
"lasuite.marketing.backends.dummy.DummyBackend",
environ_name="LASUITE_MARKETING_BACKEND",
environ_prefix=None,
),
"PARAMETERS": values.DictValue(
default={},
environ_name="LASUITE_MARKETING_PARAMETERS",
environ_prefix=None,
),
}
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -50,72 +50,67 @@ msgstr "Kuzhet"
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Lenner"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Embanner"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Merour"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Perc'henn"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Strishaet"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Anavezet"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Publik"
@@ -239,8 +234,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titl"
@@ -256,188 +251,155 @@ msgstr "Restr"
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, 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:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
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:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
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:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
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:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
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:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, 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:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, 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:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, 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:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
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:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Lesen"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Bearbeiten"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Administrator"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Besitzer"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Beschränkt"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Authentifiziert"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Öffentlich"
@@ -239,8 +234,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "Titel"
@@ -256,188 +251,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, 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:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
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:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
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:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
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:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr ""
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr ""
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr ""
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr ""
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr ""
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr ""
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr ""
@@ -239,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -256,188 +251,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Tipo de Cuerpo"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Lector"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Editor"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Administrador"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Propietario"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Restringido"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Autentificado"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Público"
@@ -239,8 +234,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "título"
@@ -256,188 +251,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
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:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
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:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
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:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
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:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
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:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -50,72 +50,67 @@ msgstr "Masqué"
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Corps"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Type de corps"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Lecteur"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr "Commentateur"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Éditeur"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Administrateur"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Propriétaire"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Restreint"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Authentifié"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Public"
@@ -239,8 +234,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titre"
@@ -256,188 +251,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, 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:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
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:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
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:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
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:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
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:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, 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:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, 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:1314 core/models.py:1314
msgid "Thread"
msgstr "Conversation"
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr "Conversations"
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr "Anonyme"
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr "Commentaire"
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr "Commentaires"
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr "Cet émoji a déjà été réagi à ce commentaire."
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr "Réaction"
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr "Réactions"
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
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:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Lettore"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Editor"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Amministratore"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Proprietario"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Limitato"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Autenticato"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Pubblico"
@@ -239,8 +234,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titolo"
@@ -256,188 +251,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, 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:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -50,72 +50,67 @@ msgstr "Gemaskeerd"
msgid "Favorite"
msgstr "Favoriet"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Lezer"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr "Commentator"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Redacteur"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Beheerder"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Eigenaar"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Beperkt"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Geauthenticeerd"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Publiek"
@@ -239,8 +234,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titel"
@@ -256,188 +251,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met u gedeeld!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, 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:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr "Een link bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "De gebruiker bestaat al in dit document."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
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:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "Document verzoekt om toegangen"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr "Kanaal"
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr "Kanalen"
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr "Anoniem"
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr "Reactie"
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr "Reacties"
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr "Deze emoji is al op deze opmerking gereageerd."
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr "Reactie"
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr "Reacties"
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Sjabloon"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Sjabloon"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Sjabloon/gebruiker relatie"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Sjabloon/gebruiker relaties"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit sjabloon."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit sjabloon."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Tipo de corpo"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Leitor"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Editor"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Administrador"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Dono"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Restrito"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Autenticado"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Público"
@@ -239,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -256,188 +251,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -50,72 +50,67 @@ msgstr "Скрытый"
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Текст сообщения"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Тип сообщения"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Читатель"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr "Комментатор"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Редактор"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Администратор"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Владелец"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Доступ ограничен"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Аутентификация выполнена"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Доступно всем"
@@ -239,8 +234,8 @@ msgstr "пользователь"
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "заголовок"
@@ -256,188 +251,155 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr "Обсуждение"
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr "Обсуждения"
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr "Аноним"
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr "Комментарий"
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr "Комментарии"
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr "Этот эмодзи уже использован в этом комментарии."
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr "Реакция"
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr "Реакции"
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Bralec"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Urednik"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Skrbnik"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Lastnik"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Omejeno"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Preverjeno"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Javno"
@@ -239,8 +234,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "naslov"
@@ -256,188 +251,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, 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:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
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:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
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:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr ""
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr ""
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Administratör"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr ""
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr ""
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr ""
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Publik"
@@ -239,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -256,188 +251,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -50,72 +50,67 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr ""
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr ""
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr ""
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr ""
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr ""
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr ""
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr ""
@@ -239,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -256,188 +251,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -50,72 +50,67 @@ msgstr "Приховано"
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Вміст"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Тип вмісту"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Читач"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr "Комментар"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Редактор"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Адміністратор"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Власник"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Обмежено"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Підтверджено"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Публічне"
@@ -239,8 +234,8 @@ msgstr "користувач"
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "заголовок"
@@ -256,188 +251,155 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr "Обговорення"
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr "Обговорення"
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr "Анонім"
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr "Коментар"
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr "Коментарі"
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr "Цим емодзі вже відреагували на цей коментар."
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr "Реакція"
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr "Реакції"
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
"PO-Revision-Date: 2025-12-09 11:12\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -50,72 +50,67 @@ msgstr "已屏蔽"
msgid "Favorite"
msgstr "收藏"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文档!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文档的所有权:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "必填字段。"
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "阅读者"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "编辑者"
#: build/lib/core/choices.py:46 core/choices.py:46
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "超级管理员"
#: build/lib/core/choices.py:47 core/choices.py:47
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "所有者"
#: build/lib/core/choices.py:58 core/choices.py:58
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "受限的"
#: build/lib/core/choices.py:62 core/choices.py:62
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "已验证"
#: build/lib/core/choices.py:64 core/choices.py:64
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "公开"
@@ -239,8 +234,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
#: core/models.py:361 core/models.py:1430
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "标题"
@@ -256,188 +251,155 @@ msgstr "文档"
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
#: core/models.py:824
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:869 core/models.py:869
#: build/lib/core/models.py:867 core/models.py:867
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:969 core/models.py:969
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:968 core/models.py:968
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:974 core/models.py:974
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:999 core/models.py:999
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:998 core/models.py:998
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1004 core/models.py:1004
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1028 core/models.py:1028
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1027 core/models.py:1027
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1033 core/models.py:1033
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1039 core/models.py:1039
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
#: core/models.py:1047 core/models.py:1516
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for access"
msgstr "文档需要访问权限"
#: build/lib/core/models.py:1199 core/models.py:1199
#: build/lib/core/models.py:1192 core/models.py:1192
msgid "Document ask for accesses"
msgstr "文档需要访问权限"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "This user has already asked for access to this document."
msgstr "用户已申请该文档的访问权限。"
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 申请访问文档!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 申请访问以下文档:"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name}申请文档:{title}的访问权限"
#: build/lib/core/models.py:1314 core/models.py:1314
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1315 core/models.py:1315
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
#: core/models.py:1318 core/models.py:1370
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1365 core/models.py:1365
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1366 core/models.py:1366
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1415 core/models.py:1415
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1420 core/models.py:1420
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1431 core/models.py:1431
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1432 core/models.py:1432
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1433 core/models.py:1433
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1435 core/models.py:1435
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1443 core/models.py:1443
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1444 core/models.py:1444
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1497 core/models.py:1497
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1498 core/models.py:1498
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1504 core/models.py:1504
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1510 core/models.py:1510
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1587 core/models.py:1587
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1606 core/models.py:1606
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1607 core/models.py:1607
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1627 core/models.py:1627
#: build/lib/core/models.py:1473 core/models.py:1473
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 = "4.2.0"
version = "3.10.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -34,12 +34,12 @@ dependencies = [
"django-countries==8.1.0",
"django-csp==4.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.22",
"django-lasuite[all]==0.0.18",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.9",
"django==5.2.8",
"django-treebeard==4.7.1",
"djangorestframework==3.16.1",
"drf_spectacular==0.29.0",

View File

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

View File

@@ -1,12 +1,7 @@
import { expect, test } from '@playwright/test';
import {
closeHeaderMenu,
createDoc,
getOtherBrowserName,
verifyDocName,
} from './utils-common';
import { getEditor, writeInEditor } from './utils-editor';
import { createDoc, getOtherBrowserName, verifyDocName } from './utils-common';
import { writeInEditor } from './utils-editor';
import {
addNewMember,
connectOtherUserToDoc,
@@ -48,7 +43,6 @@ test.describe('Doc Comments', () => {
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
await editor.first().click();
await editor.getByText('Hello').click();
await thread.getByText('This is a comment').first().hover();
@@ -122,7 +116,8 @@ test.describe('Doc Comments', () => {
await createDoc(page, 'comment-interaction', browserName, 1);
// Checks add react reaction
const editor = await writeInEditor({ page, text: 'Hello' });
const editor = page.locator('.ProseMirror');
await editor.locator('.bn-block-outer').last().fill('Hello World');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
@@ -136,7 +131,6 @@ test.describe('Doc Comments', () => {
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await editor.first().click();
await editor.getByText('Hello').click();
await thread.getByText('This is a comment').first().hover();
@@ -187,29 +181,6 @@ test.describe('Doc Comments', () => {
'background-color',
'rgba(0, 0, 0, 0)',
);
/* Delete the last comment remove the thread */
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
await thread.getByRole('paragraph').first().fill('This is a new comment');
await thread.locator('[data-test="save"]').click();
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await editor.first().click();
await editor.getByText('Hello').click();
await thread.getByText('This is a new comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
});
test('it checks the comments abilities', async ({ page, browserName }) => {
@@ -220,8 +191,6 @@ test.describe('Doc Comments', () => {
// We share the doc with another user
const otherBrowserName = getOtherBrowserName(browserName);
const editor = await getEditor({ page });
// Add a new member with editor role
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor', otherBrowserName);
@@ -245,7 +214,7 @@ test.describe('Doc Comments', () => {
text: 'Hello, I can edit the document',
});
await expect(
editor.getByText('Hello, I can edit the document'),
otherEditor.getByText('Hello, I can edit the document'),
).toBeVisible();
await otherEditor.getByText('Hello').selectText();
await otherPage.getByRole('button', { name: 'Comment' }).click();
@@ -323,71 +292,4 @@ test.describe('Doc Comments', () => {
await cleanup();
});
test('it checks comments pasting from another document', async ({
page,
browserName,
}) => {
await createDoc(page, 'comment-doc-1', browserName, 1);
// We add a comment in the first document
const editor1 = await writeInEditor({ page, text: 'Document One' });
await editor1.getByText('Document One').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
const thread1 = page.locator('.bn-thread');
await thread1.getByRole('paragraph').first().fill('Comment in Doc One');
await thread1.locator('[data-test="save"]').click();
await expect(thread1.getByText('Comment in Doc One').first()).toBeHidden();
await expect(editor1.getByText('Document One')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await editor1.getByText('Document One').click();
// We copy the content including the comment from the first document
await editor1.getByText('Document One').selectText();
await page.keyboard.press('Control+C');
// We create a second document
await createDoc(page, 'comment-doc-2', browserName, 1);
// We paste the content into the second document
const editor2 = await writeInEditor({ page, text: '' });
await editor2.click();
await page.keyboard.press('Control+V');
await expect(editor2.getByText('Document One')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await editor2.getByText('Document One').click();
await expect(page.locator('.bn-thread')).toBeHidden();
});
});
test.describe('Doc Comments mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test('Comments are not visible on mobile', async ({ page, browserName }) => {
const [title] = await createDoc(
page,
'comment-mobile',
browserName,
1,
true,
);
await closeHeaderMenu(page);
await verifyDocName(page, title);
// Checks add react reaction
const editor = await writeInEditor({ page, text: 'Hello' });
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Comment' })).toBeHidden();
await expect(page.getByRole('button', { name: 'Paragraph' })).toBeVisible();
});
});

View File

@@ -7,7 +7,6 @@ import {
randomName,
verifyDocName,
} from './utils-common';
import { connectOtherUserToDoc } from './utils-share';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -74,82 +73,6 @@ test.describe('Doc Create', () => {
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
});
test('it creates a doc with link "/doc/new/', async ({
page,
browserName,
}) => {
test.slow();
// Private doc creation
await page.goto('/docs/new/?title=My+private+doc+from+url');
await verifyDocName(page, 'My private doc from url');
await page.getByRole('button', { name: 'Share' }).click();
await expect(
page.getByTestId('doc-visibility').getByText('Private').first(),
).toBeVisible();
// Public editing doc creation
await page.goto(
'/docs/new/?title=My+public+doc+from+url&link-reach=public&link-role=editor',
);
await verifyDocName(page, 'My public doc from url');
await page.getByRole('button', { name: 'Share' }).click();
await expect(
page.getByTestId('doc-visibility').getByText('Public').first(),
).toBeVisible();
await expect(
page.getByTestId('doc-access-mode').getByText('Editing').first(),
).toBeVisible();
// Authenticated reading doc creation
await page.goto(
'/docs/new/?title=My+authenticated+doc+from+url&link-reach=authenticated&link-role=reader',
);
await verifyDocName(page, 'My authenticated doc from url');
await page.getByRole('button', { name: 'Share' }).click();
await expect(
page.getByTestId('doc-visibility').getByText('Connected').first(),
).toBeVisible();
await expect(
page.getByTestId('doc-access-mode').getByText('Reading').first(),
).toBeVisible();
const { cleanup, otherPage, otherBrowserName } =
await connectOtherUserToDoc({
docUrl:
'/docs/new/?title=From+unlogged+doc+from+url&link-reach=authenticated&link-role=reader',
browserName,
withoutSignIn: true,
});
await keyCloakSignIn(otherPage, otherBrowserName, false);
await verifyDocName(otherPage, 'From unlogged doc from url');
await otherPage.getByRole('button', { name: 'Share' }).click();
await expect(
otherPage.getByTestId('doc-visibility').getByText('Connected').first(),
).toBeVisible();
await expect(
otherPage.getByTestId('doc-access-mode').getByText('Reading').first(),
).toBeVisible();
await cleanup();
});
});
test.describe('Doc Create: Not logged', () => {

View File

@@ -24,13 +24,15 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Doc Editor', () => {
test('it checks toolbar buttons are displayed', async ({
test('it checks default toolbar buttons are displayed', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
const editor = await writeInEditor({ page, text: 'test content' });
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('test content');
await editor
.getByText('test content', {
@@ -39,9 +41,6 @@ test.describe('Doc Editor', () => {
.selectText();
const toolbar = page.locator('.bn-formatting-toolbar');
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeVisible();
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
await expect(toolbar.locator('button[data-test="italic"]')).toBeVisible();
await expect(
@@ -64,53 +63,6 @@ test.describe('Doc Editor', () => {
await expect(
toolbar.locator('button[data-test="createLink"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="ai-actions"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="convertMarkdown"]'),
).toBeVisible();
await page.keyboard.press('Escape');
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toHaveAttribute('role', 'presentation');
await image.dblclick();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="ai-actions"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="convertMarkdown"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="editcaption"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="downloadfile"]'),
).toBeVisible();
});
/**
@@ -289,66 +241,20 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
});
test('it cannot edit if viewer but see and can get resources', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(page, 'doc-viewer', browserName, 1);
await verifyDocName(page, docTitle);
await writeInEditor({ page, text: 'Hello World' });
await page.getByRole('button', { name: 'Share' }).click();
await updateShareLink(page, 'Public', 'Reading');
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
const { otherPage, cleanup } = await connectOtherUserToDoc({
browserName,
docUrl: page.url(),
withoutSignIn: true,
docTitle,
test('it cannot edit if viewer', async ({ page }) => {
await mockedDocument(page, {
user_role: 'reader',
});
await expect(
otherPage.getByLabel('It is the card information').getByText('Reader'),
).toBeVisible();
await goToGridDoc(page);
// Cannot edit
const editor = otherPage.locator('.ProseMirror');
const card = page.getByLabel('It is the card information');
await expect(card).toBeVisible();
await expect(card.getByText('Reader')).toBeVisible();
const editor = page.locator('.ProseMirror');
await expect(editor).toHaveAttribute('contenteditable', 'false');
// Owner add a image
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
// Owner see the image
await expect(
page.locator('.--docs--editor-container img.bn-visual-media').first(),
).toBeVisible();
// Viewser see the image
const viewerImg = otherPage
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(viewerImg).toBeVisible();
// Viewer can download the image
await viewerImg.click();
const downloadPromise = otherPage.waitForEvent('download');
await otherPage.getByRole('button', { name: 'Download image' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('logo-suite-numerique.png');
await cleanup();
});
test('it adds an image to the doc editor', async ({ page, browserName }) => {
@@ -802,8 +708,6 @@ test.describe('Doc Editor', () => {
await page.getByText('Symbols').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🛃' })).toBeVisible();
await page.keyboard.press('Escape');
await page.locator('.bn-side-menu > button').last().click();
await page.locator('.mantine-Menu-dropdown > button').last().click();
await page.locator('.bn-color-picker-dropdown > button').last().click();
@@ -998,44 +902,4 @@ test.describe('Doc Editor', () => {
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('test-pdf.pdf');
});
test('it preserves text when switching between mobile and desktop views', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(
page,
'doc-viewport-test',
browserName,
1,
);
await verifyDocName(page, docTitle);
const editor = await writeInEditor({
page,
text: 'Hello World - Desktop Text',
});
await expect(editor.getByText('Hello World - Desktop Text')).toBeVisible();
await page.waitForTimeout(500);
// Switch to mobile viewport
await page.setViewportSize({ width: 500, height: 1200 });
await page.waitForTimeout(500);
await expect(editor.getByText('Hello World - Desktop Text')).toBeVisible();
await writeInEditor({
page,
text: 'Mobile Text',
});
await page.waitForTimeout(500);
// Switch back to desktop viewport
await page.setViewportSize({ width: 1280, height: 720 });
await page.waitForTimeout(500);
await expect(editor.getByText('Mobile Text')).toBeVisible();
});
});

View File

@@ -2,7 +2,6 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import JSZip from 'jszip';
import { PDFParse } from 'pdf-parse';
import {
@@ -32,7 +31,7 @@ test.describe('Doc Export', () => {
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
page.getByText(/Download your document in a \.docx, \.odt.*format\./i),
page.getByText('Download your document in a .docx, .odt or .pdf format.'),
).toBeVisible();
await expect(
page.getByRole('combobox', { name: 'Template' }),
@@ -188,89 +187,6 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.odt`);
});
test('it exports the doc to html zip', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(
page,
'doc-editor-html-zip',
browserName,
1,
);
await verifyDocName(page, randomDoc);
// Add some content and at least one image so that the ZIP contains media files.
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello HTML ZIP');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
// Wait for the image to be attached and have a valid src (aria-hidden prevents toBeVisible on Chromium)
await expect(image).toBeAttached({ timeout: 10000 });
await expect(image).toHaveAttribute('src', /.*\.svg/);
// Give some time for the image to be fully processed
await page.waitForTimeout(1000);
await page
.getByRole('button', {
name: 'Export the document',
})
.click();
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'HTML' }).click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.zip`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.zip`);
const zipBuffer = await cs.toBuffer(await download.createReadStream());
// Unzip and inspect contents
const zip = await JSZip.loadAsync(zipBuffer);
// Check that index.html exists
const indexHtml = zip.file('index.html');
expect(indexHtml).not.toBeNull();
// Read and verify HTML content
const htmlContent = await indexHtml!.async('string');
expect(htmlContent).toContain('Hello HTML ZIP');
expect(htmlContent).toContain('href="styles.css"');
// Check for media files (they are at the root of the ZIP, not in a media/ folder)
// Media files are named like "1-test.svg" or "media-1.png" by deriveMediaFilename
const allFiles = Object.keys(zip.files);
const mediaFiles = allFiles.filter(
(name) => name !== 'index.html' && !name.endsWith('/'),
);
expect(mediaFiles.length).toBeGreaterThan(0);
// Verify the SVG image is included
const svgFile = mediaFiles.find((name) => name.endsWith('.svg'));
expect(svgFile).toBeDefined();
const styleFile = mediaFiles.find((name) => name === 'styles.css');
expect(styleFile).toBeDefined();
});
/**
* This test tell us that the export to pdf is working with images
* but it does not tell us if the images are being displayed correctly

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, mockedListDocs, toggleHeaderMenu } from './utils-common';
import { createDoc, mockedListDocs } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc grid dnd', () => {
@@ -185,7 +185,10 @@ test.describe('Doc grid dnd mobile', () => {
true,
);
await toggleHeaderMenu(page);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
await expect(page.locator('.--docs-sub-page-item').first()).toHaveAttribute(
'draggable',

View File

@@ -208,7 +208,7 @@ test.describe('Doc Header', () => {
await expect(
invitationCard.getByText('test.test@invitation.test').first(),
).toBeVisible();
const invitationRole = invitationCard.getByTestId('doc-role-dropdown');
const invitationRole = invitationCard.getByLabel('doc-role-dropdown');
await expect(invitationRole).toBeVisible();
await invitationRole.click();
@@ -217,7 +217,7 @@ test.describe('Doc Header', () => {
await expect(invitationCard).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
const roles = memberCard.getByTestId('doc-role-dropdown');
const roles = memberCard.getByLabel('doc-role-dropdown');
await expect(memberCard).toBeVisible();
await expect(
memberCard.getByText('test.test@accesses.test').first(),
@@ -408,6 +408,40 @@ test.describe('Doc Header', () => {
expect(clipboardContent.trim()).toBe('# Hello World');
});
test('It checks the copy as HTML button', async ({ page, browserName }) => {
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
);
// create page and navigate to it
await page
.getByRole('button', {
name: 'New doc',
})
.click();
// Add dummy content to the doc
const editor = page.locator('.ProseMirror');
const docFirstBlock = editor.locator('.bn-block-content').first();
await docFirstBlock.click();
await page.keyboard.type('# Hello World', { delay: 100 });
const docFirstBlockContent = docFirstBlock.locator('h1');
await expect(docFirstBlockContent).toHaveText('Hello World');
// Copy content to clipboard
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Copy as HTML' }).click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
// Test that clipboard is in HTML format
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(),
);
const clipboardContent = await handle.jsonValue();
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
});
test('it checks the copy link button', async ({ page, browserName }) => {
test.skip(
browserName === 'webkit',
@@ -548,12 +582,8 @@ test.describe('Doc Header', () => {
await child.hover();
await child.getByText(`more_horiz`).click();
const currentUrl = page.url();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(page).not.toHaveURL(new RegExp(currentUrl));
await verifyDocName(page, duplicateTitle);
await expect(

View File

@@ -74,7 +74,7 @@ test.describe('Document create member', () => {
await expect(list.getByText(email)).toBeVisible();
// Check roles are displayed
await list.getByTestId('doc-role-dropdown').click();
await list.getByLabel('doc-role-dropdown').click();
await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible();
@@ -84,7 +84,7 @@ test.describe('Document create member', () => {
// Validate
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByTestId('doc-share-invite-button').click();
await page.getByRole('button', { name: /^Invite / }).click();
// Check invitation added
await expect(
@@ -128,14 +128,14 @@ test.describe('Document create member', () => {
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByTestId('doc-role-dropdown').click();
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Owner' }).click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
response.url().includes('/invitations/') && response.status() === 201,
);
await page.getByTestId('doc-share-invite-button').click();
await page.getByRole('button', { name: /^Invite / }).click();
// Check invitation sent
@@ -146,7 +146,7 @@ test.describe('Document create member', () => {
await page.getByTestId(`search-user-row-${email}`).click();
// Choose a role
await container.getByTestId('doc-role-dropdown').click();
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Owner' }).click();
const responsePromiseCreateInvitationFail = page.waitForResponse(
@@ -154,7 +154,7 @@ test.describe('Document create member', () => {
response.url().includes('/invitations/') && response.status() === 400,
);
await page.getByTestId('doc-share-invite-button').click();
await page.getByRole('button', { name: /^Invite / }).click();
await expect(
page.getByText(`"${email}" is already invited to the document.`),
).toBeVisible();
@@ -183,7 +183,7 @@ test.describe('Document create member', () => {
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByTestId('doc-role-dropdown').click();
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
const responsePromiseCreateInvitation = page.waitForResponse(
@@ -191,7 +191,7 @@ test.describe('Document create member', () => {
response.url().includes('/invitations/') && response.status() === 201,
);
await page.getByTestId('doc-share-invite-button').click();
await page.getByRole('button', { name: /^Invite / }).click();
// Check invitation sent
const responseCreateInvitation = await responsePromiseCreateInvitation;
@@ -210,14 +210,18 @@ test.describe('Document create member', () => {
response.request().method() === 'PATCH',
);
await userInvitation.getByTestId('doc-role-dropdown').click();
await userInvitation.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
const responsePatchInvitation = await responsePromisePatchInvitation;
expect(responsePatchInvitation.ok()).toBeTruthy();
await userInvitation.getByTestId('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Remove access' }).click();
const moreActions = userInvitation.getByRole('button', {
name: 'Open invitation actions menu',
});
await moreActions.click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await expect(userInvitation).toBeHidden();
});
@@ -268,7 +272,7 @@ test.describe('Document create member', () => {
const container = page.getByTestId(
`doc-share-access-request-row-${emailRequest}`,
);
await container.getByTestId('doc-role-dropdown').click();
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await container.getByRole('button', { name: 'Approve' }).click();

View File

@@ -152,7 +152,7 @@ test.describe('Document list members', () => {
const currentUser = list.getByTestId(
`doc-share-member-row-user.test@${browserName}.test`,
);
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await expect(currentUser).toBeVisible();
await expect(currentUserRole).toBeVisible();
await currentUserRole.click();
@@ -169,7 +169,7 @@ test.describe('Document list members', () => {
});
const newUserEmail = await addNewMember(page, 0, 'Owner');
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
const newUserRoles = newUser.getByTestId('doc-role-dropdown');
const newUserRoles = newUser.getByLabel('doc-role-dropdown');
await expect(newUser).toBeVisible();
@@ -214,7 +214,9 @@ test.describe('Document list members', () => {
const emailMyself = `user.test@${browserName}.test`;
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
const mySelfRole = mySelf.getByTestId('doc-role-dropdown');
const mySelfRole = mySelf.getByRole('button', {
name: 'doc-role-dropdown',
});
const userOwnerEmail = await addNewMember(page, 0, 'Owner');
const userOwner = list.getByTestId(
@@ -229,7 +231,9 @@ test.describe('Document list members', () => {
const userReader = list.getByTestId(
`doc-share-member-row-${userReaderEmail}`,
);
const userReaderRole = userReader.getByTestId('doc-role-dropdown');
const userReaderRole = userReader.getByRole('button', {
name: 'doc-role-dropdown',
});
await expect(mySelf).toBeVisible();
await expect(userOwner).toBeVisible();

View File

@@ -19,8 +19,6 @@ test.describe('Doc Table Content', () => {
await page.locator('.ProseMirror').click();
await expect(page.getByRole('button', { name: 'Summary' })).toBeHidden();
await page.keyboard.type('# Level 1\n## Level 2\n### Level 3');
const summaryContainer = page.locator('#summaryContainer');

View File

@@ -226,7 +226,7 @@ test.describe('Doc Tree', () => {
const currentUser = list.getByTestId(
`doc-share-member-row-user.test@${browserName}.test`,
);
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await currentUserRole.click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await list.click();
@@ -303,40 +303,6 @@ test.describe('Doc Tree', () => {
await expect(docTree.getByText(docChild)).toBeVisible();
});
test('keyboard navigation with F2 focuses root actions button', async ({
page,
browserName,
}) => {
// Create a parent document to initialize the tree
const [docParent] = await createDoc(
page,
'doc-tree-keyboard-f2-root',
browserName,
1,
);
await verifyDocName(page, docParent);
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible();
const rootItem = page.getByTestId('doc-tree-root-item');
await expect(rootItem).toBeVisible();
// Focus the root item
await rootItem.focus();
await expect(rootItem).toBeFocused();
// Press F2 → focus should move to the root actions \"More options\" button
await page.keyboard.press('F2');
const rootActions = rootItem.locator('.doc-tree-root-item-actions');
const rootMoreOptionsButton = rootActions.getByRole('button', {
name: /more options/i,
});
await expect(rootMoreOptionsButton).toBeFocused();
});
test('it updates the child icon from the tree', async ({
page,
browserName,

View File

@@ -6,7 +6,6 @@ import {
mockedDocument,
verifyDocName,
} from './utils-common';
import { openSuggestionMenu, writeInEditor } from './utils-editor';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -18,7 +17,6 @@ test.describe('Doc Version', () => {
await verifyDocName(page, randomDoc);
// Initially, there is no version
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await expect(page.getByText('History', { exact: true })).toBeVisible();
@@ -28,49 +26,31 @@ test.describe('Doc Version', () => {
await expect(panel).toBeVisible();
await expect(modal.getByText('No versions')).toBeVisible();
const editor = page.locator('.ProseMirror');
await modal.getByRole('button', { name: 'close' }).click();
await editor.click();
await page.keyboard.type('# Hello World');
await writeInEditor({ page, text: 'Hello World' });
// It will trigger a save, no version created yet (initial version is not counted)
await goToGridDoc(page, {
title: randomDoc,
});
await expect(page.getByText('Hello World')).toBeVisible();
// Write more
await writeInEditor({ page, text: 'It will create a version' });
await openSuggestionMenu({ page });
await page.getByText('Add a callout block').click();
const calloutBlock = page
.locator('div[data-content-type="callout"]')
.first();
await expect(calloutBlock).toBeVisible();
// It will trigger a save and create a version this time
await goToGridDoc(page, {
title: randomDoc,
});
await expect(page.getByText('Hello World')).toBeHidden();
await expect(page.getByText('It will create a version')).toBeVisible();
await expect(calloutBlock).toBeVisible();
// Write more
await writeInEditor({ page, text: 'It will create a second version' });
// It will trigger a save and create a second version
await goToGridDoc(page, {
title: randomDoc,
});
await expect(
page.getByText('It will create a second version'),
page.getByRole('heading', { name: 'Hello World' }),
).toBeVisible();
await page
.locator('.ProseMirror .bn-block')
.getByRole('heading', { name: 'Hello World' })
.fill('It will create a version');
await goToGridDoc(page, {
title: randomDoc,
});
await expect(page.getByText('Hello World')).toBeHidden();
await expect(
page.getByRole('heading', { name: 'It will create a version' }),
).toBeVisible();
await page.getByLabel('Open the document options').click();
@@ -80,33 +60,11 @@ test.describe('Doc Version', () => {
await expect(page.getByText('History', { exact: true })).toBeVisible();
await expect(page.getByRole('status')).toBeHidden();
const items = await panel.locator('.version-item').all();
expect(items.length).toBe(2);
await items[1].click();
await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeHidden();
await expect(
modal.locator('div[data-content-type="callout"]').first(),
).toBeHidden();
expect(items.length).toBe(1);
await items[0].click();
await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeVisible();
await expect(
modal.locator('div[data-content-type="callout"]').first(),
).toBeVisible();
await expect(
modal.getByText('It will create a second version'),
).toBeHidden();
await items[1].click();
await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeHidden();
await expect(
modal.locator('div[data-content-type="callout"]').first(),
).toBeHidden();
});
test('it does not display the doc versions if not allowed', async ({

View File

@@ -156,10 +156,9 @@ test.describe('Header: Override configuration', () => {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
icon: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
height: 'auto',
alt: '',
},
},
@@ -177,27 +176,3 @@ test.describe('Header: Override configuration', () => {
await expect(logoImage).toHaveAttribute('alt', '');
});
});
test.describe('Header: Skip to Content', () => {
test('it displays skip link on first TAB and focuses main content on click', async ({
page,
}) => {
await page.goto('/');
// Wait for skip button to be mounted (client-side only component)
const skipButton = page.getByRole('button', { name: 'Go to content' });
await skipButton.waitFor({ state: 'attached' });
// First TAB shows the skip button
await page.keyboard.press('Tab');
// The skip button should be visible and focused
await expect(skipButton).toBeFocused();
await expect(skipButton).toBeVisible();
// Clicking moves focus to the main content
await skipButton.click();
const mainContent = page.locator('main#mainContent');
await expect(mainContent).toBeFocused();
});
});

View File

@@ -66,7 +66,6 @@ test.describe('Language', () => {
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import { createDoc } from './utils-common';
test.describe('Left panel desktop', () => {
test.beforeEach(async ({ page }) => {
@@ -10,12 +10,8 @@ test.describe('Left panel desktop', () => {
test('checks all the elements are visible', async ({ page }) => {
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
await expect(page.getByTestId('home-button')).toBeHidden();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
await goToGridDoc(page);
await expect(page.getByTestId('home-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
});
test('checks resize handle is present and functional on document page', async ({
@@ -92,9 +88,6 @@ test.describe('Left panel mobile', () => {
await expect(languageButton).not.toBeInViewport();
await expect(logoutButton).not.toBeInViewport();
const title = await goToGridDoc(page);
await verifyDocName(page, title);
await header.getByLabel('Open the header menu').click();
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();

View File

@@ -83,34 +83,6 @@ export const randomName = (name: string, browserName: string, length: number) =>
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
});
export const openHeaderMenu = async (page: Page) => {
const toggleButton = page.getByTestId('header-menu-toggle');
await expect(toggleButton).toBeVisible();
const isExpanded =
(await toggleButton.getAttribute('aria-expanded')) === 'true';
if (!isExpanded) {
await toggleButton.click();
}
};
export const closeHeaderMenu = async (page: Page) => {
const toggleButton = page.getByTestId('header-menu-toggle');
await expect(toggleButton).toBeVisible();
const isExpanded =
(await toggleButton.getAttribute('aria-expanded')) === 'true';
if (isExpanded) {
await toggleButton.click();
}
};
export const toggleHeaderMenu = async (page: Page) => {
const toggleButton = page.getByTestId('header-menu-toggle');
await expect(toggleButton).toBeVisible();
await toggleButton.click();
};
export const createDoc = async (
page: Page,
docName: string,
@@ -122,7 +94,10 @@ export const createDoc = async (
for (let i = 0; i < randomDocs.length; i++) {
if (isMobile) {
await openHeaderMenu(page);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
}
await page

View File

@@ -38,9 +38,9 @@ export const addNewMember = async (
await page.getByRole('option', { name: users[index].email }).click();
// Choose a role
await page.getByTestId('doc-role-dropdown').click();
await page.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: role }).click();
await page.getByTestId('doc-share-invite-button').click();
await page.getByRole('button', { name: /^Invite / }).click();
return users[index].email;
};
@@ -74,7 +74,7 @@ export const updateRoleUser = async (
const list = page.getByTestId('doc-share-quick-search');
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await currentUserRole.click();
await page.getByRole('menuitem', { name: role }).click();
await list.click();

View File

@@ -2,8 +2,6 @@ import { Page, expect } from '@playwright/test';
import {
BrowserName,
closeHeaderMenu,
openHeaderMenu,
randomName,
updateDocTitle,
verifyDocName,
@@ -17,7 +15,10 @@ export const createRootSubPage = async (
isMobile = false,
) => {
if (isMobile) {
await openHeaderMenu(page);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
}
// Get response
@@ -28,7 +29,10 @@ export const createRootSubPage = async (
const subPageJson = (await response.json()) as { id: string };
if (isMobile) {
await openHeaderMenu(page);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
}
// Get doc tree
@@ -40,9 +44,13 @@ export const createRootSubPage = async (
.getByTestId(`doc-sub-page-item-${subPageJson.id}`)
.first();
await expect(subPageItem).toBeVisible();
await subPageItem.click();
if (isMobile) {
await closeHeaderMenu(page);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('close')
.click();
}
// Update sub page name

View File

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

View File

@@ -6,7 +6,7 @@ server {
root /usr/share/nginx/html;
location / {
try_files $uri index.html $uri/index.html =404;
try_files $uri index.html $uri/ =404;
add_header X-Frame-Options DENY always;
}

View File

@@ -1,25 +1,57 @@
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
import { defaultTokens } from '@openfun/cunningham-react';
import merge from 'lodash/merge';
// Uikit does not provide the full list of tokens.
// To be able to override correctly, we need to merge with the default tokens.
let mergedColors = merge(
defaultTokens.globals.colors,
tokens.themes.default.globals.colors,
);
mergedColors = {
...mergedColors,
'logo-1': '#2845C1',
const customColors = {
'primary-action': '#1212FF',
'primary-bg': '#FAFAFA',
'primary-focus': '#0A76F6',
'secondary-icon': 'var(--c--theme--colors--primary-text)',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
'brown-400': '#E6BE92',
'brown-500': '#BD987A',
'brown-600': '#745B47',
'cyan-400': '#34BAB5',
'cyan-500': '#009099',
'cyan-600': '#006A6F',
'gold-400': '#FFCA00',
'gold-500': '#C3992A',
'gold-600': '#695240',
'green-400': '#34CB6A',
'green-500': '#00A95F',
'green-600': '#297254',
'olive-400': '#99C221',
'olive-500': '#68A532',
'olive-600': '#447049',
'orange-400': '#FF732C',
'orange-500': '#E4794A',
'orange-600': '#755348',
'pink-400': '#FFB7AE',
'pink-500': '#E18B76',
'pink-600': '#8D533E',
'purple-400': '#CE70CC',
'purple-500': '#A558A0',
'purple-600': '#6E445A',
'yellow-400': '#D8C634',
'yellow-500': '#B7A73F',
'yellow-600': '#66673D',
};
tokens.themes.default.globals = {
...tokens.themes.default.globals,
tokens.themes.default.theme = {
...tokens.themes.default.theme,
...{
colors: mergedColors,
logo: {
src: '',
alt: '',
widthHeader: '',
widthFooter: '',
},
colors: {
...tokens.themes.default.theme.colors,
...customColors,
},
font: {
...tokens.themes.default.globals.font,
...tokens.themes.default.theme.font,
families: {
base: 'sans-serif',
accent: 'sans-serif',
@@ -31,29 +63,39 @@ tokens.themes.default.globals = {
tokens.themes.default.components = {
...tokens.themes.default.components,
...{
logo: {
src: '',
alt: '',
widthHeader: '',
widthFooter: '',
},
'la-gaufre': false,
'home-proconnect': false,
icon: {
src: '/assets/icon-docs.svg',
width: '32px',
height: 'auto',
},
'image-system-filter': '',
favicon: {
ico: '/assets/favicon-light.ico',
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
},
button: {
...tokens.themes.default.components.button,
primary: {
...tokens.themes.default.components.button.primary,
...{
'background--disabled': 'var(--c--theme--colors--greyscale-100)',
},
disabled: 'var(--c--theme--colors--greyscale-400)',
},
},
},
};
const dsfrTheme = {
dsfr: {
globals: {
theme: {
colors: {
'secondary-icon': '#C9191E',
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
font: {
families: {
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
@@ -62,19 +104,8 @@ const dsfrTheme = {
},
},
components: {
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
'la-gaufre': true,
'home-proconnect': true,
icon: {
src: '/assets/icon-docs-dsfr.svg',
width: '32px',
height: 'auto',
},
favicon: {
ico: '/assets/favicon-dsfr.ico',
'png-light': '/assets/favicon-dsfr.png',
@@ -86,296 +117,131 @@ const dsfrTheme = {
const genericTheme = {
generic: {
globals: {
theme: {
colors: {
'brand-050': '#EEF1FA',
'brand-100': '#DDE2F5',
'brand-150': '#CED3F1',
'brand-200': '#BEC5F0',
'brand-250': '#AFB5F1',
'brand-300': '#A0A5F6',
'brand-350': '#8F94FD',
'brand-400': '#8184FC',
'brand-450': '#7576EE',
'brand-500': '#6969DF',
'brand-550': '#5E5CD0',
'brand-600': '#534FC2',
'brand-650': '#4844AD',
'brand-700': '#3E3B98',
'brand-750': '#36347D',
'brand-800': '#2D2F5F',
'brand-850': '#262848',
'brand-900': '#1C1E32',
'brand-950': '#11131F',
'gray-000': '#FFFFFF',
'gray-025': '#F8F8F9',
'gray-050': '#F0F0F3',
'gray-100': '#E2E2EA',
'gray-150': '#D3D4E0',
'gray-200': '#C5C6D5',
'gray-250': '#B7B7CB',
'gray-300': '#A9A9BF',
'gray-350': '#9C9CB2',
'gray-400': '#8F8FA4',
'gray-450': '#828297',
'gray-500': '#75758A',
'gray-550': '#69697D',
'gray-600': '#5D5D70',
'gray-650': '#515164',
'gray-700': '#454558',
'gray-750': '#3A3A4C',
'gray-800': '#2F303D',
'gray-850': '#25252F',
'gray-900': '#1B1B23',
'gray-950': '#111114',
'gray-1000': '#000000',
'info-050': '#EAF2F9',
'info-100': '#D5E4F3',
'info-150': '#BFD7F0',
'info-200': '#A7CAEE',
'info-250': '#8DBDEF',
'info-300': '#6EB0F2',
'info-350': '#50A2F5',
'info-400': '#3593F4',
'info-450': '#1185ED',
'info-500': '#0077DE',
'info-550': '#0069CF',
'info-600': '#005BC0',
'info-650': '#0D4EAA',
'info-700': '#124394',
'info-750': '#163878',
'info-800': '#192F5A',
'info-850': '#192541',
'info-900': '#141B2D',
'info-950': '#0C111C',
'success-050': '#E8F1EA',
'success-100': '#CFE4D4',
'success-150': '#BAD9C1',
'success-200': '#A2CFAD',
'success-250': '#86C597',
'success-300': '#6CBA83',
'success-350': '#4FB070',
'success-400': '#40A363',
'success-450': '#309556',
'success-500': '#1E884A',
'success-550': '#027B3E',
'success-600': '#016D31',
'success-650': '#006024',
'success-700': '#005317',
'success-750': '#0D4511',
'success-800': '#11380E',
'success-850': '#132A11',
'success-900': '#101E0F',
'success-950': '#091209',
'warning-050': '#F8F0E9',
'warning-100': '#F1E0D3',
'warning-150': '#ECD0BC',
'warning-200': '#E8C0A4',
'warning-250': '#E8AE8A',
'warning-300': '#EB9970',
'warning-350': '#E98456',
'warning-400': '#E57036',
'warning-450': '#DA5E18',
'warning-500': '#CB5000',
'warning-550': '#BC4200',
'warning-600': '#AD3300',
'warning-650': '#9E2300',
'warning-700': '#882011',
'warning-750': '#731E16',
'warning-800': '#58201A',
'warning-850': '#401D18',
'warning-900': '#2E1714',
'warning-950': '#1D0F0D',
'error-050': '#F9EFEC',
'error-100': '#F4DFD9',
'error-150': '#F0CEC6',
'error-200': '#EEBCB2',
'error-250': '#EEA99D',
'error-300': '#EF9486',
'error-350': '#F37C6E',
'error-400': '#F65F53',
'error-450': '#F0463D',
'error-500': '#E82322',
'error-550': '#D7010E',
'error-600': '#C00100',
'error-650': '#AA0000',
'error-700': '#910C06',
'error-750': '#731E16',
'error-800': '#58201A',
'error-850': '#401D18',
'error-900': '#2E1714',
'error-950': '#1D0F0D',
'red-050': '#FAEFEE',
'red-100': '#F4DEDD',
'red-150': '#F1CDCB',
'red-200': '#EFBBBA',
'red-250': '#EEA8A8',
'red-300': '#F09394',
'red-350': '#F37B7E',
'red-400': '#EF6569',
'red-450': '#E94A55',
'red-500': '#DA3B49',
'red-550': '#CA2A3C',
'red-600': '#BB1330',
'red-650': '#A90021',
'red-700': '#910A13',
'red-750': '#731E16',
'red-800': '#58201A',
'red-850': '#411D18',
'red-900': '#2E1714',
'red-950': '#1D0F0D',
'orange-050': '#F8F0E9',
'orange-100': '#F1E0D3',
'orange-150': '#ECD0BD',
'orange-200': '#EABFA6',
'orange-250': '#EBAC90',
'orange-300': '#EC9772',
'orange-350': '#E5845A',
'orange-400': '#D6774D',
'orange-450': '#C86A40',
'orange-500': '#B95D33',
'orange-550': '#AB5025',
'orange-600': '#9D4315',
'orange-650': '#8F3600',
'orange-700': '#812900',
'orange-750': '#6C2511',
'orange-800': '#572017',
'orange-850': '#401D18',
'orange-900': '#2E1714',
'orange-950': '#1D0F0D',
'brown-050': '#F6F0E8',
'brown-100': '#F1E0D3',
'brown-150': '#EBD0BA',
'brown-200': '#E2C0A6',
'brown-250': '#D4B398',
'brown-300': '#C6A58B',
'brown-350': '#B8987E',
'brown-400': '#AA8B71',
'brown-450': '#9D7E65',
'brown-500': '#8F7158',
'brown-550': '#82654C',
'brown-600': '#765841',
'brown-650': '#694C35',
'brown-700': '#5D412A',
'brown-750': '#51361E',
'brown-800': '#452A13',
'brown-850': '#392008',
'brown-900': '#29180A',
'brown-950': '#1B0F08',
'yellow-050': '#F3F0E7',
'yellow-100': '#E9E2CF',
'yellow-150': '#E1D4B7',
'yellow-200': '#D9C599',
'yellow-250': '#D2B677',
'yellow-300': '#CAA756',
'yellow-350': '#C2972E',
'yellow-400': '#B98900',
'yellow-450': '#AB7B00',
'yellow-500': '#9D6E00',
'yellow-550': '#916100',
'yellow-600': '#855400',
'yellow-650': '#784700',
'yellow-700': '#6C3A00',
'yellow-750': '#5F2E00',
'yellow-800': '#512302',
'yellow-850': '#3E1D10',
'yellow-900': '#2D1711',
'yellow-950': '#1D0F0D',
'green-050': '#E6F1E9',
'green-100': '#CFE4D5',
'green-150': '#B8D8C1',
'green-200': '#A0CFAE',
'green-250': '#84C59A',
'green-300': '#65BA86',
'green-350': '#45B173',
'green-400': '#23A562',
'green-450': '#029755',
'green-500': '#008948',
'green-550': '#017B3B',
'green-600': '#006E2E',
'green-650': '#006022',
'green-700': '#005314',
'green-750': '#0D4510',
'green-800': '#11380E',
'green-850': '#132A11',
'green-900': '#101E0F',
'green-950': '#091209',
'blue1-050': '#EBF1F9',
'blue1-100': '#D6E4F4',
'blue1-150': '#C1D7F0',
'blue1-200': '#AACAEF',
'blue1-250': '#8FBCEF',
'blue1-300': '#7CAFEB',
'blue1-350': '#68A1E4',
'blue1-400': '#5B94D6',
'blue1-450': '#4E86C7',
'blue1-500': '#4279B9',
'blue1-550': '#356CAC',
'blue1-600': '#28609E',
'blue1-650': '#1B5390',
'blue1-700': '#0B4783',
'blue1-750': '#0F3C6E',
'blue1-800': '#133059',
'blue1-850': '#152641',
'blue1-900': '#121C2D',
'blue1-950': '#0B111C',
'blue2-050': '#E7F3F4',
'blue2-100': '#CEE7E9',
'blue2-150': '#B2DCE0',
'blue2-200': '#91D1D7',
'blue2-250': '#68C7D0',
'blue2-300': '#43BBC5',
'blue2-350': '#00AFBA',
'blue2-400': '#01A0AA',
'blue2-450': '#00929D',
'blue2-500': '#00848F',
'blue2-550': '#007682',
'blue2-600': '#016874',
'blue2-650': '#005B67',
'blue2-700': '#004E5A',
'blue2-750': '#00424E',
'blue2-800': '#003642',
'blue2-850': '#002A38',
'blue2-900': '#061E28',
'blue2-950': '#071219',
'purple-050': '#F7F0F6',
'purple-100': '#EEE0EE',
'purple-150': '#E7D1E7',
'purple-200': '#DBBFE4',
'purple-250': '#D3AEE2',
'purple-300': '#CB99E1',
'purple-350': '#C188D9',
'purple-400': '#B47BCB',
'purple-450': '#A66EBD',
'purple-500': '#9961AF',
'purple-550': '#8B55A1',
'purple-600': '#7E4894',
'purple-650': '#723C87',
'purple-700': '#633376',
'purple-750': '#552A65',
'purple-800': '#452551',
'purple-850': '#35213D',
'purple-900': '#261A2C',
'purple-950': '#17111C',
'pink-050': '#F8EFF4',
'pink-100': '#F0DFEA',
'pink-150': '#EACEDF',
'pink-200': '#E9BBD1',
'pink-250': '#E9A7C2',
'pink-300': '#E095B4',
'pink-350': '#D685A8',
'pink-400': '#C7799B',
'pink-450': '#B86C8D',
'pink-500': '#AA5F80',
'pink-550': '#9C5374',
'pink-600': '#8E4767',
'pink-650': '#813B5B',
'pink-700': '#732E4F',
'pink-750': '#632643',
'pink-800': '#521F38',
'pink-850': '#3E1C2B',
'pink-900': '#2D171F',
'pink-950': '#1C0E12',
'primary-action': '#206EBD',
'primary-focus': '#1E64BF',
'primary-text': '#2E2C28',
'primary-050': '#F8F8F7',
'primary-100': '#F0EFEC',
'primary-150': '#F4F4FD',
'primary-200': '#E8E7E4',
'primary-300': '#CFCDC9',
'primary-400': '#979592',
'primary-500': '#82807D',
'primary-600': '#3F3D39',
'primary-700': '#2E2C28',
'primary-800': '#302E29',
'primary-900': '#282622',
'primary-950': '#201F1C',
'secondary-text': '#fff',
'secondary-50': '#F4F7FA',
'secondary-100': '#D7E3EE',
'secondary-200': '#B8CCE1',
'secondary-300': '#99B4D3',
'secondary-400': '#7595BE',
'secondary-500': '#5874A0',
'secondary-600': '#3A5383',
'secondary-700': '#1E3462',
'secondary-800': '#091B41',
'secondary-900': '#08183B',
'secondary-950': '#071636',
'greyscale-text': '#3C3B38',
'greyscale-000': '#fff',
'greyscale-050': '#F8F7F7',
'greyscale-100': '#F3F3F2',
'greyscale-200': '#ECEBEA',
'greyscale-250': '#E4E3E2',
'greyscale-300': '#D3D2CF',
'greyscale-350': '#eee',
'greyscale-400': '#96948E',
'greyscale-500': '#817E77',
'greyscale-600': '#6A6862',
'greyscale-700': '#3C3B38',
'greyscale-750': '#383632',
'greyscale-800': '#2D2B27',
'greyscale-900': '#262522',
'greyscale-950': '#201F1C',
'greyscale-1000': '#181714',
'success-text': '#234935',
'success-50': '#F3FBF5',
'success-100': '#E4F7EA',
'success-200': '#CAEED4',
'success-300': '#A0E0B5',
'success-400': '#6CC88C',
'success-500': '#6CC88C',
'success-600': '#358D5C',
'success-700': '#2D704B',
'success-800': '#28583F',
'success-900': '#234935',
'success-950': '#0F281B',
'info-text': '#212445',
'info-50': '#F2F6FB',
'info-100': '#E2E9F5',
'info-200': '#CCD8EE',
'info-300': '#A9C0E3',
'info-400': '#809DD4',
'info-500': '#617BC7',
'info-600': '#4A5CBF',
'info-700': '#3E49B2',
'info-800': '#353C8F',
'info-900': '#303771',
'info-950': '#212445',
'warning-text': '#D97C3A',
'warning-50': '#FDF7F1',
'warning-100': '#FBEDDC',
'warning-200': '#F5D9B9',
'warning-300': '#EDBE8C',
'warning-400': '#E2985C',
'warning-500': '#D97C3A',
'warning-600': '#C96330',
'warning-700': '#A34B32',
'warning-800': '#813B2C',
'warning-900': '#693327',
'warning-950': '#381713',
'danger-action': '#C0182A',
'danger-text': '#FFF',
'danger-050': '#FDF5F4',
'danger-100': '#FBEBE8',
'danger-200': '#F9E0DC',
'danger-300': '#F3C3BD',
'danger-400': '#E26552',
'danger-500': '#C91F00',
'danger-600': '#A71901',
'danger-700': '#562C2B',
'danger-800': '#392425',
'danger-900': '#311F20',
'danger-950': '#2A191A',
'blue-400': '#8BAECC',
'blue-500': '#567AA2',
'blue-600': '#455784',
'brown-400': '#E4C090',
'brown-500': '#BA9977',
'brown-600': '#735C45',
'cyan-400': '#5CBEC9',
'cyan-500': '#43A1B3',
'cyan-600': '#39809B',
'gold-400': '#ECBF50',
'gold-500': '#DFA038',
'gold-600': '#C17B31',
'green-400': '#5DBD9A',
'green-500': '#3AA183',
'green-600': '#2A816D',
'olive-400': '#AFD662',
'olive-500': '#90BB4B',
'olive-600': '#6E9441',
'orange-400': '#E2985C',
'orange-500': '#D97C3A',
'orange-600': '#C96330',
'pink-400': '#BE8FC8',
'pink-500': '#A563B1',
'pink-600': '#8B44A5',
'purple-400': '#BE8FC8',
'purple-500': '#A563B1',
'purple-600': '#8B44A5',
'yellow-400': '#EDC947',
'yellow-500': '#DBB13A',
'yellow-600': '#B88A34',
},
font: {
families: {
@@ -384,6 +250,18 @@ const genericTheme = {
},
},
},
components: {
button: {
primary: {
background: {
'color-hover': 'var(--c--theme--colors--primary-focus)',
'color-active': 'var(--c--theme--colors--primary-focus)',
'color-focus': 'var(--c--theme--colors--primary-focus)',
},
},
},
'image-system-filter': 'saturate(0.2)',
},
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "4.2.0",
"version": "3.10.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -19,30 +19,30 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.44.2",
"@blocknote/core": "0.44.2",
"@blocknote/mantine": "0.44.2",
"@blocknote/react": "0.44.2",
"@blocknote/xl-docx-exporter": "0.44.2",
"@blocknote/xl-multi-column": "0.44.2",
"@blocknote/xl-odt-exporter": "0.44.2",
"@blocknote/xl-pdf-exporter": "0.44.2",
"@blocknote/code-block": "0.42.3",
"@blocknote/core": "0.42.3",
"@blocknote/mantine": "0.42.3",
"@blocknote/react": "0.42.3",
"@blocknote/xl-docx-exporter": "0.42.3",
"@blocknote/xl-multi-column": "0.42.3",
"@blocknote/xl-odt-exporter": "0.42.3",
"@blocknote/xl-pdf-exporter": "0.42.3",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.30",
"@fontsource-variable/material-symbols-outlined": "5.2.28",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.18.0",
"@gouvfr-lasuite/ui-kit": "0.16.2",
"@hocuspocus/provider": "3.4.0",
"@mantine/core": "8.3.9",
"@mantine/hooks": "8.3.9",
"@openfun/cunningham-react": "4.0.0",
"@mantine/core": "8.3.6",
"@mantine/hooks": "8.3.6",
"@openfun/cunningham-react": "3.2.3",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.27.0",
"@tanstack/react-query": "5.90.10",
"@sentry/nextjs": "10.22.0",
"@tanstack/react-query": "5.90.6",
"@tiptap/extensions": "*",
"canvg": "4.0.3",
"clsx": "2.1.1",
@@ -52,17 +52,17 @@
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.6.3",
"i18next": "25.6.0",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.2",
"next": "15.5.9",
"posthog-js": "1.298.0",
"next": "15.5.4",
"posthog-js": "1.284.0",
"react": "*",
"react-aria-components": "1.13.0",
"react-dom": "*",
"react-i18next": "16.3.5",
"react-i18next": "16.3.3",
"react-intersection-observer": "10.0.0",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
@@ -74,32 +74,32 @@
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.91.0",
"@tanstack/react-query-devtools": "5.90.2",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/lodash": "4.17.21",
"@types/lodash": "4.17.20",
"@types/luxon": "3.7.1",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.1.1",
"@vitejs/plugin-react": "5.1.0",
"copy-webpack-plugin": "13.0.1",
"cross-env": "10.1.0",
"dotenv": "17.2.3",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "27.2.0",
"jsdom": "27.1.0",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.26.0",
"stylelint": "16.25.0",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13",
"webpack": "5.103.0",
"vitest": "4.0.6",
"webpack": "5.102.1",
"workbox-webpack-plugin": "7.1.0"
},
"packageManager": "yarn@1.22.22"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,12 +0,0 @@
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="#C9191E"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
fill="#2845C1"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,4 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.6305 28.8312C22.7983 28.5038 23.9166 27.9062 24.6505 26.8503C25.3749 25.8163 25.5789 24.5047 25.5789 23.2425V4.75099C25.5789 4.42358 25.5611 4.09557 25.5216 3.77148C26.1016 3.99961 26.5486 4.37658 26.8626 4.90239C27.2331 5.50024 27.4184 6.28757 27.4184 7.26435V26.0464C27.4184 27.3684 27.0942 28.3578 26.4458 29.0146C25.7974 29.6714 24.8207 29.9998 23.5155 29.9998H16.4209C16.5889 29.9704 16.7574 29.9401 16.9262 29.909C18.4067 29.6444 19.9713 29.2854 21.6185 28.8346L21.6305 28.8312Z" fill="#4747A3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58203 25.655V6.8477C4.58203 5.70251 4.88938 4.83519 5.50408 4.24575C6.1272 3.65631 6.95242 3.33212 7.97972 3.27318C9.49542 3.18055 10.9311 3.05425 12.2868 2.89425C13.6425 2.72584 14.9393 2.53217 16.1771 2.31324C17.4234 2.0943 18.6359 1.85011 19.8148 1.58065C21.0274 1.29435 21.9578 1.4375 22.6062 2.0101C23.2546 2.58269 23.5788 3.49632 23.5788 4.75099V23.2425C23.5788 24.3456 23.3893 25.1666 23.0104 25.7055C22.6315 26.2529 21.9915 26.6528 21.0905 26.9054C19.4906 27.3433 17.9833 27.6886 16.5687 27.9412C15.154 28.2022 13.7731 28.4001 12.4258 28.5348C11.0785 28.6696 9.69751 28.7748 8.28286 28.8506C7.11241 28.918 6.20299 28.6738 5.5546 28.118C4.90622 27.5707 4.58203 26.7497 4.58203 25.655ZM9.20865 10.2624C11.0635 10.1444 12.7632 9.96305 14.3075 9.71831C14.6822 9.65722 15.0564 9.5936 15.4291 9.52759C15.8192 9.45851 16.1013 9.11859 16.1013 8.72337C16.1013 8.21154 15.638 7.82609 15.135 7.91189C14.846 7.96118 14.5555 8.00909 14.2635 8.05562C12.7346 8.29923 11.0452 8.47998 9.19523 8.5977C8.91819 8.61558 8.69776 8.70188 8.55608 8.87391C8.42209 9.03661 8.35645 9.23229 8.35645 9.45535C8.35645 9.68212 8.43296 9.87951 8.58568 10.0418L8.58783 10.0439C8.75336 10.2095 8.96369 10.2811 9.20865 10.2624ZM9.20801 14.456C11.0631 14.338 12.763 14.1566 14.3075 13.9119C15.8588 13.6589 17.3936 13.3638 18.9112 13.0266C19.2191 12.9581 19.4498 12.8503 19.5652 12.683C19.6786 12.5221 19.7347 12.3376 19.7347 12.1332C19.7347 11.9026 19.6469 11.704 19.476 11.5426C19.2921 11.3689 19.0348 11.3284 18.7304 11.3911L18.7285 11.3915C17.2823 11.7194 15.794 12.0053 14.2635 12.2492C12.7346 12.4928 11.0452 12.6735 9.19523 12.7913C8.91819 12.8091 8.69776 12.8954 8.55608 13.0675C8.42276 13.2294 8.35645 13.4205 8.35645 13.6363C8.35645 13.8703 8.43209 14.0723 8.58558 14.2354L8.59 14.2396C8.75499 14.3949 8.96316 14.4655 9.20551 14.4562L9.20801 14.456ZM9.20847 18.6494C11.0634 18.5229 12.7631 18.3374 14.3075 18.0927C15.8589 17.8482 17.3934 17.5573 18.9112 17.22C19.2199 17.1514 19.4508 17.0391 19.566 16.8627C19.6783 16.7029 19.7347 16.5233 19.7347 16.3266C19.7347 16.0961 19.6469 15.8974 19.476 15.7361C19.2921 15.5623 19.0348 15.5218 18.7304 15.5845L18.729 15.5848C17.2827 15.9043 15.7942 16.1861 14.2635 16.43C12.7345 16.6736 11.045 16.8586 9.19495 16.9847C8.91804 17.0026 8.69771 17.0889 8.55608 17.2609C8.42276 17.4228 8.35645 17.6139 8.35645 17.8297C8.35645 18.0637 8.43209 18.2658 8.58558 18.4289L8.59 18.433C8.75499 18.5883 8.96316 18.6589 9.20551 18.6496L9.20847 18.6494ZM14.3075 22.257C12.7632 22.5018 11.0635 22.6831 9.20867 22.8012C8.9637 22.8198 8.75337 22.7482 8.58783 22.5826L8.58572 22.5805C8.433 22.4182 8.35645 22.2208 8.35645 21.9941C8.35645 21.771 8.42209 21.5753 8.55608 21.4126C8.69776 21.2406 8.91827 21.1543 9.19531 21.1364C11.0453 21.0187 12.7346 20.838 14.2635 20.5943C14.5555 20.5478 14.846 20.4999 15.135 20.4506C15.638 20.3648 16.1013 20.7503 16.1013 21.2621C16.1013 21.6573 15.8192 21.9972 15.4291 22.0663C15.0564 22.1323 14.6822 22.1959 14.3075 22.257Z" fill="#4747A3"/>
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="#C9191E"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
fill="#000091"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,7 +1,7 @@
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="var(--c--globals--colors--red-600)"
fill="var(--c--theme--colors--secondary-icon)"
/>
<path
fill-rule="evenodd"

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -4,8 +4,6 @@ import { CSSProperties, RuleSet } from 'styled-components/dist/types';
import {
MarginPadding,
Spacings,
spacingValue,
stylesMargin,
stylesPadding,
} from '@/utils/styleBuilder';
@@ -16,7 +14,6 @@ export interface BoxProps {
as?: HTMLElementType;
$align?: CSSProperties['alignItems'];
$background?: CSSProperties['background'];
$border?: CSSProperties['border'];
$color?: CSSProperties['color'];
$css?: string | RuleSet<object>;
$cursor?: CSSProperties['cursor'];
@@ -24,7 +21,7 @@ export interface BoxProps {
$display?: CSSProperties['display'];
$effect?: 'show' | 'hide';
$flex?: CSSProperties['flex'];
$gap?: Spacings;
$gap?: CSSProperties['gap'];
$hasTransition?: boolean | 'slow';
$height?: CSSProperties['height'];
$justify?: CSSProperties['justifyContent'];
@@ -39,46 +36,53 @@ export interface BoxProps {
$position?: CSSProperties['position'];
$radius?: CSSProperties['borderRadius'];
$shrink?: CSSProperties['flexShrink'];
$transition?: CSSProperties['transition'];
$width?: CSSProperties['width'];
$zIndex?: CSSProperties['zIndex'];
$wrap?: CSSProperties['flexWrap'];
// Theming props
$layer?: 'background' | 'content' | 'border';
$theme?:
| 'brand'
| 'error'
| 'gray'
| 'primary'
| 'primary-text'
| 'secondary'
| 'secondary-text'
| 'info'
| 'success'
| 'warning'
| 'neutral'
| 'contextual'
| 'disabled'
| (string & {});
$scope?: 'surface' | 'semantic' | 'palette' | (string & {});
$variation?: 'primary' | 'secondary' | 'tertiary' | (string & {});
$withThemeBG?: boolean;
$withThemeBorder?: boolean;
$withThemeInherited?: boolean;
| 'danger'
| 'greyscale';
$transition?: CSSProperties['transition'];
$variation?:
| 'text'
| '000'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '1000';
$width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap'];
$zIndex?: CSSProperties['zIndex'];
}
export type BoxType = ComponentPropsWithRef<typeof Box>;
export const Box = styled('div')<BoxProps>`
${({ $align }) => $align && `align-items: ${$align};`}
${({ $background }) => $background && `background: ${$background};`}
${({ $color }) => $color && `color: ${$color};`}
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
${({ $display, as }) =>
`display: ${$display || (as?.match('span|input') ? 'inline-flex' : 'flex')};`}
${({ $flex }) => $flex && `flex: ${$flex};`}
${({ $gap }) => $gap && `gap: ${spacingValue($gap)};`}
${({ $gap }) => $gap && `gap: ${$gap};`}
${({ $height }) => $height && `height: ${$height};`}
${({ $hasTransition }) =>
$hasTransition && $hasTransition === 'slow'
? `transition: all 0.5s var(--c--globals--transitions--ease-out);`
? `transition: all 0.5s ease-in-out;`
: $hasTransition
? `transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);`
? `transition: all 0.3s ease-in-out;`
: ''}
${({ $justify }) => $justify && `justify-content: ${$justify};`}
${({ $margin }) => $margin && stylesMargin($margin)}
@@ -92,85 +96,11 @@ export const Box = styled('div')<BoxProps>`
${({ $position }) => $position && `position: ${$position};`}
${({ $radius }) => $radius && `border-radius: ${$radius};`}
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
${({
$layer = 'border',
$theme = 'brand',
$variation = 'primary',
$scope = 'semantic',
$border,
$withThemeBorder,
$withThemeInherited,
}) => {
if ($border) {
return `border: ${$border};`;
}
if (!$layer || !$scope || !$theme || !$withThemeBorder) {
${({ $theme, $variation }) => {
if (!$theme || !$variation) {
return '';
}
if ($withThemeInherited) {
return `border: inherit;`;
}
return `border: 1px solid var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({
$layer = 'background',
$theme = 'brand',
$variation = 'primary',
$scope = 'semantic',
$background,
$withThemeBG,
$withThemeInherited,
}) => {
if ($background) {
return `background: ${$background};`;
}
if (!$layer || !$scope || !$theme || !$withThemeBG) {
return '';
}
if ($withThemeInherited) {
return `background: inherit;`;
}
return `background: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({
$layer = 'content',
$theme = 'neutral',
$variation = 'primary',
$scope = 'semantic',
$color,
$withThemeBG,
$withThemeInherited,
}) => {
if ($color) {
return `color: ${$color};`;
}
if (!$layer || !$scope) {
return '';
}
// There is a special case when primary with background
if (
$withThemeBG &&
$layer === 'content' &&
$scope === 'semantic' &&
$variation === 'primary' &&
$theme
) {
$variation = `on-${$theme}`;
}
if ($withThemeInherited) {
return `color: inherit;`;
}
return `color: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
return `color: var(--c--theme--colors--${$theme}-${$variation});`;
}}
${({ $transition }) => $transition && `transition: ${$transition};`}
${({ $width }) => $width && `width: ${$width};`}
@@ -191,7 +121,7 @@ export const Box = styled('div')<BoxProps>`
return (
effect &&
`
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);
transition: all 0.3s ease-in-out;
${effect}
`
);

View File

@@ -24,8 +24,8 @@ export type BoxButtonType = BoxType & {
*/
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
({ $css, ...props }, ref) => {
const theme = props.$theme || 'gray';
const variation = props.$variation || 'primary';
const theme = props.$theme || 'greyscale';
const variation = props.$variation || '400';
return (
<Box
@@ -40,14 +40,15 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
border: none;
outline: none;
font-family: inherit;
color: ${props.disabled &&
`var(--c--contextuals--content--semantic--disabled--primary)`};
color: ${props.disabled
? `var(--c--theme--colors--${theme}-400) !important`
: `inherit`};
&:focus-visible {
transition: none;
outline: 2px solid
var(--c--contextuals--content--semantic--${theme}--${variation});
outline: 2px solid var(--c--theme--colors--${theme}-${variation});
border-radius: 1px;
outline-offset: var(--c--globals--spacings--st);
outline-offset: 4px;
}
${$css || ''}
`}

View File

@@ -1,23 +1,27 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { Box, BoxType } from '.';
export const Card = ({
children,
className,
$css,
...props
}: PropsWithChildren<BoxType>) => {
const { colorsTokens } = useCunninghamTheme();
return (
<Box
role="region"
$withThemeBG
$withThemeBorder
className={`--docs--card ${className || ''}`}
$radius="var(--c--globals--spacings--st)"
$padding={{ horizontal: 'xs', vertical: '3xs' }}
$scope={props.$theme ? props.$scope || 'semantic' : 'surface'}
$theme={props.$theme || 'primary'}
$variation={props.$theme ? props.$variation || 'tertiary' : ''}
$background="white"
$radius="4px"
$css={css`
border: 1px solid ${colorsTokens['greyscale-200']};
${$css}
`}
{...props}
>
{children}

View File

@@ -14,11 +14,10 @@ import { BoxProps } from './Box';
const StyledPopover = styled(Popover)`
background-color: white;
border-radius: var(--c--globals--spacings--st);
box-shadow: 0 0 6px 0 rgba(0, 0, 145, 0.1);
border: 1px solid var(--c--contextuals--border--surface--primary);
transition: opacity var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
border-radius: 4px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
border: 1px solid #dddddd;
transition: opacity 0.2s ease-in-out;
`;
interface StyledButtonProps {
@@ -29,22 +28,18 @@ const StyledButton = styled(Button)<StyledButtonProps>`
border: none;
background: none;
outline: none;
font-weight: var(--c--components--button--font-weight);
font-size: var(--c--components--button--medium-font-size);
padding: var(--c--globals--spacings--0);
border-radius: var(--c--globals--spacings--st);
color: var(--c--contextuals--content--semantic--brand--tertiary);
font-weight: 500;
font-size: 0.938rem;
padding: 0;
border-radius: 4px;
&:hover {
background-color: var(
--c--contextuals--background--semantic--contextual--primary
--c--components--button--primary-text--background--color-hover
);
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--c--globals--colors--brand-400);
background-color: var(
--c--contextuals--background--semantic--brand--tertiary-hover
);
border-radius: var(--c--globals--spacings--st);
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
border-radius: 4px;
}
${({ $css }) => $css};
`;

View File

@@ -13,7 +13,7 @@ export const Icon = ({
iconName,
disabled,
variant = 'outlined',
$theme = 'neutral',
$variation = 'text',
...textProps
}: IconProps) => {
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
@@ -28,7 +28,7 @@ export const Icon = ({
'material-icons': variant === 'outlined',
'material-symbols-outlined': variant === 'symbols-outlined',
})}
$theme={disabled ? 'disabled' : $theme}
$variation={disabled ? '300' : $variation}
aria-disabled={disabled}
{...textProps}
>

View File

@@ -39,8 +39,7 @@ export const InfiniteScroll = ({
{!isLoading && hasMore && (
<Button
onClick={() => void next()}
color="brand"
variant="bordered"
color="primary-text"
icon={<Icon iconName="arrow_downward" />}
>
{buttonLabel ?? t('Load more')}

View File

@@ -24,12 +24,12 @@ export const LoadMoreText = ({
className="--docs--load-more"
>
<Icon
$theme="brand"
$variation="secondary"
$theme="primary"
$variation="800"
iconName="arrow_downward"
$size="md"
/>
<Text $theme="brand" $variation="secondary">
<Text $theme="primary" $variation="800">
{t('Load more')}
</Text>
</Box>

View File

@@ -1,78 +0,0 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
export const SkipToContent = () => {
const { t } = useTranslation();
const router = useRouter();
const { spacingsTokens } = useCunninghamTheme();
const [isVisible, setIsVisible] = useState(false);
// Reset focus after route change so first TAB goes to skip link
useEffect(() => {
const handleRouteChange = () => {
(document.activeElement as HTMLElement)?.blur();
document.body.setAttribute('tabindex', '-1');
document.body.focus({ preventScroll: true });
setTimeout(() => {
document.body.removeAttribute('tabindex');
}, 100);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
const mainContent = document.getElementById(MAIN_LAYOUT_ID);
if (mainContent) {
mainContent.focus();
mainContent.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<Box
$css={css`
.c__button--brand--primary.--docs--skip-to-content:focus-visible {
box-shadow:
0 0 0 1px var(--c--globals--colors--white-000),
0 0 0 4px var(--c--contextuals--border--semantic--brand--primary);
border-radius: var(--c--globals--spacings--st);
}
`}
>
<Button
onClick={handleClick}
type="button"
color="brand"
className="--docs--skip-to-content"
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
style={{
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none',
position: 'fixed',
top: spacingsTokens['2xs'],
// padding header + logo(32px) + gap(3xs≈4px) + text "Docs"(≈70px) + 12px
left: `calc(${spacingsTokens['base']} + 32px + ${spacingsTokens['3xs']} + 70px + 12px)`,
zIndex: 9999,
whiteSpace: 'nowrap',
}}
>
{t('Go to content')}
</Button>
</Box>
);
};

View File

@@ -5,7 +5,7 @@ import { tokens } from '@/cunningham';
import { Box, BoxProps } from './Box';
const { sizes } = tokens.themes.default.globals.font;
const { sizes } = tokens.themes.default.theme.font;
type TextSizes = keyof typeof sizes;
export interface TextProps extends BoxProps {
@@ -27,14 +27,24 @@ export const TextStyled = styled(Box)<TextProps>`
${({ $size }) =>
$size &&
`font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`}
${({ $color }) => $color && `color: ${$color};`}
${({ $ellipsis }) =>
$ellipsis &&
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}
`;
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
(props, ref) => {
return <TextStyled ref={ref} as="span" {...props} />;
({ className, ...props }, ref) => {
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={className}
{...props}
/>
);
},
);

View File

@@ -7,7 +7,7 @@ import { Box, Text, TextType } from '@/components';
const AlertStyled = styled(Alert)`
& .c__button--tertiary:hover {
background-color: var(--c--globals--colors--gray-200);
background-color: var(--c--theme--colors--greyscale-200);
}
`;
@@ -54,7 +54,8 @@ export const TextOnlyErrors = ({
causes.map((cause, i) => (
<Text
key={`causes-${i}`}
$theme="error"
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
@@ -63,7 +64,12 @@ export const TextOnlyErrors = ({
))}
{!causes && (
<Text $theme="error" $textAlign="center" {...textProps}>
<Text
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{defaultMessage || t('Something bad happens, please retry.')}
</Text>
)}

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