Compare commits

..

2 Commits

Author SHA1 Message Date
Cyril 012b06f3b1 (e2e) adds an end to end test for heading accessibility in doc editor
adds an end to end test to verify progressive heading suggestions

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-07-31 17:49:23 +02:00
Cyril 0c8bf4013a ️(frontend) fix heading insertion via useHeadingAccessibilityFilter
enforces contiguous heading levels to ensure accessibility compliance

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-07-31 17:47:34 +02:00
112 changed files with 4799 additions and 4643 deletions
+6 -28
View File
@@ -10,31 +10,8 @@ and this project adheres to
### Added
- ✨(backend) Comments on text editor #1309
### Changed
- ⚡️(frontend) improve accessibility:
- #1248
- #1235
- #1275
- #1255
- #1262
- #1244
- #1270
- #1282
### Fixed
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
- 🐛(minio) fix user permission error with Minio and Windows #1264
## [3.5.0] - 2025-07-31
### Added
- ✨(helm) Service Account support for K8s Resources in Helm Charts #780
- ✨(backend) allow masking documents from the list view #1172
- ✨(helm) Service Account support for K8s Resources in Helm Charts #778
- ✨(backend) allow masking documents from the list view #1171
- ✨(frontend) subdocs can manage link reach #1190
- ✨(frontend) add duplicate action to doc tree #1175
- ✨(frontend) Interlinking doc #904
@@ -46,7 +23,9 @@ and this project adheres to
- ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200
- ⚡️(frontend) improve tree stability #1207
- ⚡️(frontend) improve accessibility #1232
- ⚡️(frontend) improve accessibility
- #1232
- #1251
- 🛂(frontend) block drag n drop when not desktop #1239
### Fixed
@@ -699,8 +678,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.5.0...main
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.4.2...main
[v3.4.2]: https://github.com/suitenumerique/docs/releases/v3.4.2
[v3.4.1]: https://github.com/suitenumerique/docs/releases/v3.4.1
[v3.4.0]: https://github.com/suitenumerique/docs/releases/v3.4.0
+4 -8
View File
@@ -35,13 +35,9 @@ DB_PORT = 5432
# -- Docker
# Get the current user ID to use for docker run and docker exec commands
ifeq ($(OS),Windows_NT)
DOCKER_USER := 0:0 # run containers as root on Windows
else
DOCKER_UID := $(shell id -u)
DOCKER_GID := $(shell id -g)
DOCKER_USER := $(DOCKER_UID):$(DOCKER_GID)
endif
DOCKER_UID = $(shell id -u)
DOCKER_GID = $(shell id -g)
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f compose.yml -f compose-e2e.yml
COMPOSE_EXEC = $(COMPOSE) exec
@@ -52,7 +48,7 @@ COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
# -- Backend
MANAGE = $(COMPOSE_RUN_APP) python manage.py
MAIL_YARN = $(COMPOSE_RUN) -w //app/src/mail node yarn
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn
# -- Frontend
PATH_FRONT = ./src/frontend
-4
View File
@@ -38,10 +38,6 @@ function _set_user() {
# options: docker compose command options
# ARGS : docker compose command arguments
function _docker_compose() {
# Set DOCKER_USER for Windows compatibility with MinIO
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "${WSL_DISTRO_NAME:-}" ]]; then
export DOCKER_USER="0:0"
fi
echo "🐳(compose) file: '${COMPOSE_FILE}'"
docker compose \
@@ -11,9 +11,6 @@ server {
server_name localhost;
charset utf-8;
# increase max upload size
client_max_body_size 10m;
# Disables server version feedback on pages and in headers
server_tokens off;
@@ -71,7 +68,7 @@ server {
proxy_set_header Host $host;
}
location /collaboration/api/ {
location /collaboration/api/ {
# Collaboration server
proxy_pass http://${YPROVIDER_HOST}:4444;
proxy_set_header Host $host;
@@ -98,7 +95,7 @@ server {
add_header Content-Security-Policy "default-src 'none'" always;
}
location /media-auth {
proxy_pass http://docs_backend/api/v1.0/documents/media-auth/;
proxy_set_header X-Forwarded-Proto https;
@@ -112,4 +109,4 @@ server {
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
}
}
+15 -15
View File
@@ -7,12 +7,12 @@ services:
timeout: 2s
retries: 300
env_file:
- env.d/postgresql
- env.d/common
- env.d/postgresql
- env.d/common
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ./data/databases/backend:/var/lib/postgresql/data/pgdata
- ./data/databases/backend:/var/lib/postgresql/data/pgdata
redis:
image: redis:8
@@ -22,12 +22,12 @@ services:
user: ${DOCKER_USER:-1000}
restart: always
environment:
- DJANGO_CONFIGURATION=Production
- DJANGO_CONFIGURATION=Production
env_file:
- env.d/common
- env.d/backend
- env.d/yprovider
- env.d/postgresql
- env.d/common
- env.d/backend
- env.d/yprovider
- env.d/postgresql
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 15s
@@ -45,24 +45,24 @@ services:
image: lasuite/impress-y-provider:latest
user: ${DOCKER_USER:-1000}
env_file:
- env.d/common
- env.d/yprovider
- env.d/common
- env.d/yprovider
frontend:
image: lasuite/impress-frontend:latest
user: "101"
entrypoint:
- /docker-entrypoint.sh
- /docker-entrypoint.sh
command: ["nginx", "-g", "daemon off;"]
env_file:
- env.d/common
- env.d/common
# Uncomment and set your values if using our nginx proxy example
#environment:
# - VIRTUAL_HOST=${DOCS_HOST} # used by nginx proxy
# - VIRTUAL_HOST=${DOCS_HOST} # used by nginx proxy
# - VIRTUAL_PORT=8083 # used by nginx proxy
# - LETSENCRYPT_HOST=${DOCS_HOST} # used by lets encrypt to generate TLS certificate
volumes:
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
depends_on:
backend:
condition: service_healthy
+3 -3
View File
@@ -9,9 +9,9 @@
```bash
mkdir keycloak
curl -o keycloak/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
curl -o keycloak/env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
curl -o keycloak/env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
curl -o env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
curl -o env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
```
### Step 2:. Update `env.d/` files
+7 -7
View File
@@ -7,23 +7,23 @@ services:
timeout: 2s
retries: 300
env_file:
- env.d/kc_postgresql
- env.d/kc_postgresql
volumes:
- ./data/keycloak:/var/lib/postgresql/data/pgdata
- ./data/keycloak:/var/lib/postgresql/data/pgdata
keycloak:
image: quay.io/keycloak/keycloak:26.1.3
command: ["start"]
env_file:
- env.d/kc_postgresql
- env.d/keycloak
- env.d/kc_postgresql
- env.d/keycloak
# Uncomment and set your values if using our nginx proxy example
# environment:
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
# - VIRTUAL_PORT=8080 # used by nginx proxy
# - LETSENCRYPT_HOST=id.yourdomain.tld # used by lets encrypt to generate TLS certificate
depends_on:
kc_postgresql:
kc_postgresql::
condition: service_healthy
restart: true
# Uncomment if using our nginx proxy example
@@ -33,4 +33,4 @@ services:
#
#networks:
# proxy-tier:
# external: true
# external: true
+1 -1
View File
@@ -9,7 +9,7 @@
```bash
mkdir minio
curl -o minio/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
```
### Step 2:. Update compose file with your own values
+5 -5
View File
@@ -2,8 +2,8 @@ services:
minio:
image: minio/minio
environment:
- MINIO_ROOT_USER=<set minio root username>
- MINIO_ROOT_PASSWORD=<set minio root password>
- MINIO_ROOT_USER=<set minio root username>
- MINIO_ROOT_PASSWORD=<set minio root password>
# Uncomment and set your values if using our nginx proxy example
# - VIRTUAL_HOST=storage.yourdomain.tld # used by nginx proxy
# - VIRTUAL_PORT=9000 # used by nginx proxy
@@ -16,12 +16,12 @@ services:
entrypoint: ""
command: minio server /data
volumes:
- ./data/minio:/data
- ./data/minio:/data
# Uncomment if using our nginx proxy example
# networks:
# - proxy-tier
# - proxy-tier
# Uncomment if using our nginx proxy example
#networks:
# proxy-tier:
# external: true
# external: true
+1 -1
View File
@@ -13,7 +13,7 @@ Acme-companion is a lightweight companion container for nginx-proxy. It handles
```bash
mkdir nginx-proxy
curl -o nginx-proxy/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
```
### Step 2: Edit `DEFAULT_EMAIL` in the compose file.
+12 -12
View File
@@ -3,28 +3,28 @@ services:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
- "80:80"
- "443:443"
volumes:
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- proxy-tier
- proxy-tier
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- DEFAULT_EMAIL=mail@yourdomain.tld
- DEFAULT_EMAIL=mail@yourdomain.tld
volumes_from:
- nginx-proxy
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy-tier
- proxy-tier
networks:
proxy-tier:
+3
View File
@@ -46,6 +46,9 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
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
+1 -7
View File
@@ -31,17 +31,11 @@ For older versions of Docker Engine that do not include Docker Compose:
```bash
mkdir -p docs/env.d
cd docs
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/compose.yaml
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/common
curl -o env.d/backend https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/backend
curl -o env.d/yprovider https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/yprovider
curl -o env.d/postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
```
If you are using the sample nginx-proxy configuration:
```bash
curl -o default.conf.template https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docker/files/production/etc/nginx/conf.d/default.conf.template
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
```
## Step 2: Configuration
+3
View File
@@ -168,6 +168,9 @@ DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
### Find s3 bucket connection values
+49
View File
@@ -83,6 +83,55 @@ If you already have CRLF line endings in your local repository, the **best appro
git commit -m "✏️(project) Fix line endings to LF"
```
## Minio Permission Issues on Windows
### Problem Description
On Windows, you may encounter permission-related errors when running Minio in development mode with Docker Compose. This typically happens because:
- **Windows file permissions** don't map well to Unix-style user IDs used in Docker containers
- **Docker Desktop** may have issues with user mapping when using the `DOCKER_USER` environment variable
- **Minio container** fails to start or access volumes due to permission conflicts
### Common Symptoms
- Minio container fails to start with permission denied errors
- Error messages related to file system permissions in Minio logs
- Unable to create or access buckets in the development environment
- Docker Compose showing Minio service as unhealthy or exited
### Solution for Windows Users
If you encounter Minio permission issues on Windows, you can temporarily disable user mapping for the Minio service:
1. **Open the `compose.yml` file**
2. **Comment out the user directive** in the `minio` service section:
```yaml
minio:
# user: ${DOCKER_USER:-1000} # Comment this line on Windows if permission issues occur
image: minio/minio
environment:
- MINIO_ROOT_USER=impress
- MINIO_ROOT_PASSWORD=password
# ... rest of the configuration
```
3. **Restart the services**:
```bash
make run
```
### Why This Works
- Commenting out the `user` directive allows the Minio container to run with its default user
- This bypasses Windows-specific permission mapping issues
- The container will have the necessary permissions to access and manage the mounted volumes
### Note
This is a **development-only workaround**. In production environments, proper user mapping and security considerations should be maintained according to your deployment requirements.
## Frontend File Watching Issues on Windows
### Problem Description
+2 -2
View File
@@ -43,8 +43,8 @@ OIDC_RP_CLIENT_ID=<client_id>
OIDC_RP_CLIENT_SECRET=<client secret>
OIDC_RP_SIGN_ALGO=RS256
OIDC_RP_SCOPES="openid email"
#OIDC_USERINFO_SHORTNAME_FIELD
#OIDC_USERINFO_FULLNAME_FIELDS
#USER_OIDC_FIELD_TO_SHORTNAME
#USER_OIDC_FIELDS_TO_FULLNAME
LOGIN_REDIRECT_URL=https://${DOCS_HOST}
LOGIN_REDIRECT_URL_FAILURE=https://${DOCS_HOST}
+1 -1
View File
@@ -1,4 +1,4 @@
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api/
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api
Y_PROVIDER_API_KEY=<generate a random key>
COLLABORATION_SERVER_SECRET=<generate a random key>
COLLABORATION_SERVER_ORIGIN=https://${DOCS_HOST}
-16
View File
@@ -171,19 +171,3 @@ class ResourceAccessPermission(IsAuthenticated):
action = view.action
return abilities.get(action, False)
class CommentPermission(permissions.BasePermission):
"""Permission class for comments."""
def has_permission(self, request, view):
"""Check permission for a given object."""
if view.action in ["create", "list"]:
document_abilities = view.get_document_or_404().get_abilities(request.user)
return document_abilities["comment"]
return True
def has_object_permission(self, request, view, obj):
"""Check permission for a given object."""
return obj.get_abilities(request.user).get(view.action, False)
-44
View File
@@ -801,47 +801,3 @@ class MoveDocumentSerializer(serializers.Serializer):
choices=enums.MoveNodePositionChoices.choices,
default=enums.MoveNodePositionChoices.LAST_CHILD,
)
class CommentSerializer(serializers.ModelSerializer):
"""Serialize comments."""
user = UserLightSerializer(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.Comment
fields = [
"id",
"content",
"created_at",
"updated_at",
"user",
"document",
"abilities",
]
read_only_fields = [
"id",
"created_at",
"updated_at",
"user",
"document",
"abilities",
]
def get_abilities(self, comment) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return comment.get_abilities(request.user)
return {}
def validate(self, attrs):
"""Validate invitation data."""
request = self.context.get("request")
user = getattr(request, "user", None)
attrs["document_id"] = self.context["resource_id"]
attrs["user_id"] = user.id if user else None
return attrs
-33
View File
@@ -2072,36 +2072,3 @@ class ConfigView(drf.views.APIView):
)
return theme_customization
class CommentViewSet(
viewsets.ModelViewSet,
):
"""API ViewSet for comments."""
permission_classes = [permissions.CommentPermission]
queryset = models.Comment.objects.select_related("user", "document").all()
serializer_class = serializers.CommentSerializer
pagination_class = Pagination
_document = None
def get_document_or_404(self):
"""Get the document related to the viewset or raise a 404 error."""
if self._document is None:
try:
self._document = models.Document.objects.get(
pk=self.kwargs["resource_id"],
)
except models.Document.DoesNotExist as e:
raise drf.exceptions.NotFound("Document not found.") from e
return self._document
def get_serializer_context(self):
"""Extra context provided to the serializer class."""
context = super().get_serializer_context()
context["resource_id"] = self.kwargs["resource_id"]
return context
def get_queryset(self):
"""Return the queryset according to the action."""
return super().get_queryset().filter(document=self.kwargs["resource_id"])
-2
View File
@@ -33,7 +33,6 @@ class LinkRoleChoices(PriorityTextChoices):
"""Defines the possible roles a link can offer on a document."""
READER = "reader", _("Reader") # Can read
COMMENTATOR = "commentator", _("Commentator") # Can read and comment
EDITOR = "editor", _("Editor") # Can read and edit
@@ -41,7 +40,6 @@ class RoleChoices(PriorityTextChoices):
"""Defines the possible roles a user can have in a resource."""
READER = "reader", _("Reader") # Can read
COMMENTATOR = "commentator", _("Commentator") # Can read and comment
EDITOR = "editor", _("Editor") # Can read and edit
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
OWNER = "owner", _("Owner")
-11
View File
@@ -256,14 +256,3 @@ class InvitationFactory(factory.django.DjangoModelFactory):
document = factory.SubFactory(DocumentFactory)
role = factory.fuzzy.FuzzyChoice([role[0] for role in models.RoleChoices.choices])
issuer = factory.SubFactory(UserFactory)
class CommentFactory(factory.django.DjangoModelFactory):
"""A factory to create comments for a document"""
class Meta:
model = models.Comment
document = factory.SubFactory(DocumentFactory)
user = factory.SubFactory(UserFactory)
content = factory.Faker("text")
@@ -1,146 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-26 08:11
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0024_add_is_masked_field_to_link_trace"),
]
operations = [
migrations.AlterField(
model_name="document",
name="link_role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="documentaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="documentaskforaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="invitation",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name="templateaccess",
name="role",
field=models.CharField(
choices=[
("reader", "Reader"),
("commentator", "Commentator"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
migrations.CreateModel(
name="Comment",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("content", models.TextField()),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="core.document",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="comments",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Comment",
"verbose_name_plural": "Comments",
"db_table": "impress_comment",
"ordering": ("-created_at",),
},
),
]
+1 -50
View File
@@ -762,7 +762,6 @@ class Document(MP_Node, BaseModel):
can_update = (
is_owner_or_admin or role == RoleChoices.EDITOR
) and not is_deleted
can_comment = (can_update or role == RoleChoices.COMMENTATOR) and not is_deleted
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
@@ -787,7 +786,6 @@ class Document(MP_Node, BaseModel):
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
"comment": can_comment,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": is_owner,
@@ -1147,12 +1145,7 @@ class DocumentAccess(BaseAccess):
set_role_to = []
if is_owner_or_admin:
set_role_to.extend(
[
RoleChoices.READER,
RoleChoices.COMMENTATOR,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]
[RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN]
)
if role == RoleChoices.OWNER:
set_role_to.append(RoleChoices.OWNER)
@@ -1284,48 +1277,6 @@ class DocumentAskForAccess(BaseModel):
self.document.send_email(subject, [email], context, language)
class Comment(BaseModel):
"""User comment on a document."""
document = models.ForeignKey(
Document,
on_delete=models.CASCADE,
related_name="comments",
)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="comments",
null=True,
blank=True,
)
content = models.TextField()
class Meta:
db_table = "impress_comment"
ordering = ("-created_at",)
verbose_name = _("Comment")
verbose_name_plural = _("Comments")
def __str__(self):
author = self.user or _("Anonymous")
return f"{author!s} on {self.document!s}"
def get_abilities(self, user):
"""Compute and return abilities for a given user."""
role = self.document.get_role(user)
can_comment = self.document.get_abilities(user)["comment"]
return {
"destroy": self.user == user
or role in [RoleChoices.OWNER, RoleChoices.ADMIN],
"update": self.user == user
or role in [RoleChoices.OWNER, RoleChoices.ADMIN],
"partial_update": self.user == user
or role in [RoleChoices.OWNER, RoleChoices.ADMIN],
"retrieve": can_comment,
}
class Template(BaseModel):
"""HTML and CSS code used for formatting the print around the MarkDown body."""
@@ -292,7 +292,6 @@ def test_api_document_accesses_retrieve_set_role_to_child():
}
assert result_dict[str(document_access_other_user.id)] == [
"reader",
"commentator",
"editor",
"administrator",
"owner",
@@ -301,7 +300,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
# Add an access for the other user on the parent
parent_access_other_user = factories.UserDocumentAccessFactory(
document=parent, user=other_user, role="commentator"
document=parent, user=other_user, role="editor"
)
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
@@ -314,7 +313,6 @@ def test_api_document_accesses_retrieve_set_role_to_child():
result["id"]: result["abilities"]["set_role_to"] for result in content
}
assert result_dict[str(document_access_other_user.id)] == [
"commentator",
"editor",
"administrator",
"owner",
@@ -322,7 +320,6 @@ def test_api_document_accesses_retrieve_set_role_to_child():
assert result_dict[str(parent_access.id)] == []
assert result_dict[str(parent_access_other_user.id)] == [
"reader",
"commentator",
"editor",
"administrator",
"owner",
@@ -335,28 +332,28 @@ def test_api_document_accesses_retrieve_set_role_to_child():
[
["administrator", "reader", "reader", "reader"],
[
["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
[],
["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"],
],
],
[
["owner", "reader", "reader", "reader"],
[
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["owner", "reader", "reader", "owner"],
[
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
],
@@ -417,44 +414,44 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
[
["administrator", "reader", "reader", "reader"],
[
["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
[],
["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"],
],
],
[
["owner", "reader", "reader", "reader"],
[
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["owner", "reader", "reader", "owner"],
[
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["reader", "reader", "reader", "owner"],
[
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
[],
[],
["reader", "commentator", "editor", "administrator", "owner"],
["reader", "editor", "administrator", "owner"],
],
],
[
["reader", "administrator", "reader", "editor"],
[
["reader", "commentator", "editor", "administrator"],
["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
[],
],
@@ -462,7 +459,7 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
[
["editor", "editor", "administrator", "editor"],
[
["reader", "commentator", "editor", "administrator"],
["reader", "editor", "administrator"],
[],
["editor", "administrator"],
[],
@@ -1,588 +0,0 @@
"""Test API for comments on documents."""
import random
from django.contrib.auth.models import AnonymousUser
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
# List comments
def test_list_comments_anonymous_user_public_document():
"""Anonymous users should be allowed to list comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment1, comment2 = factories.CommentFactory.create_batch(2, document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 200
assert response.json() == {
"count": 2,
"next": None,
"previous": None,
"results": [
{
"id": str(comment2.id),
"content": comment2.content,
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment2.user.full_name,
"short_name": comment2.user.short_name,
},
"document": str(comment2.document.id),
"abilities": comment2.get_abilities(AnonymousUser()),
},
{
"id": str(comment1.id),
"content": comment1.content,
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment1.user.full_name,
"short_name": comment1.user.short_name,
},
"document": str(comment1.document.id),
"abilities": comment1.get_abilities(AnonymousUser()),
},
],
}
@pytest.mark.parametrize("link_reach", ["restricted", "authenticated"])
def test_list_comments_anonymous_user_non_public_document(link_reach):
"""Anonymous users should not be allowed to list comments on a non-public document."""
document = factories.DocumentFactory(
link_reach=link_reach, link_role=models.LinkRoleChoices.COMMENTATOR
)
factories.CommentFactory(document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 401
def test_list_comments_authenticated_user_accessible_document():
"""Authenticated users should be allowed to list comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment1 = factories.CommentFactory(document=document)
comment2 = factories.CommentFactory(document=document, user=user)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 200
assert response.json() == {
"count": 2,
"next": None,
"previous": None,
"results": [
{
"id": str(comment2.id),
"content": comment2.content,
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment2.user.full_name,
"short_name": comment2.user.short_name,
},
"document": str(comment2.document.id),
"abilities": comment2.get_abilities(user),
},
{
"id": str(comment1.id),
"content": comment1.content,
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment1.user.full_name,
"short_name": comment1.user.short_name,
},
"document": str(comment1.document.id),
"abilities": comment1.get_abilities(user),
},
],
}
def test_list_comments_authenticated_user_non_accessible_document():
"""Authenticated users should not be allowed to list comments on a non-accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
factories.CommentFactory(document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 403
def test_list_comments_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to list comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
factories.CommentFactory(document=document)
# other comments not linked to the document
factories.CommentFactory.create_batch(2)
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/comments/")
assert response.status_code == 403
# Create comment
def test_create_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to create comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 201
assert response.json() == {
"id": str(response.json()["id"]),
"content": "test",
"created_at": response.json()["created_at"],
"updated_at": response.json()["updated_at"],
"user": None,
"document": str(document.id),
"abilities": {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": True,
},
}
def test_create_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to create comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
client = APIClient()
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 401
def test_create_comment_authenticated_user_accessible_document():
"""Authenticated users should be allowed to create comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 201
assert response.json() == {
"id": str(response.json()["id"]),
"content": "test",
"created_at": response.json()["created_at"],
"updated_at": response.json()["updated_at"],
"user": {
"full_name": user.full_name,
"short_name": user.short_name,
},
"document": str(document.id),
"abilities": {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
},
}
def test_create_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to create comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/comments/", {"content": "test"}
)
assert response.status_code == 403
# Retrieve comment
def test_retrieve_comment_anonymous_user_public_document():
"""Anonymous users should be allowed to retrieve comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 200
assert response.json() == {
"id": str(comment.id),
"content": comment.content,
"created_at": comment.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": comment.updated_at.isoformat().replace("+00:00", "Z"),
"user": {
"full_name": comment.user.full_name,
"short_name": comment.user.short_name,
},
"document": str(comment.document.id),
"abilities": comment.get_abilities(AnonymousUser()),
}
def test_retrieve_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to retrieve comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_retrieve_comment_authenticated_user_accessible_document():
"""Authenticated users should be allowed to retrieve comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 200
def test_retrieve_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to retrieve comments on a document they don't have
comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
# Update comment
def test_update_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to update comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 401
def test_update_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to update comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 401
def test_update_comment_authenticated_user_accessible_document():
"""Authenticated users should not be able to update comments not their own."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted",
users=[
(
user,
random.choice(
[models.LinkRoleChoices.COMMENTATOR, models.LinkRoleChoices.EDITOR]
),
)
],
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 403
def test_update_comment_authenticated_user_own_comment():
"""Authenticated users should be able to update comments not their own."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted",
users=[
(
user,
random.choice(
[models.LinkRoleChoices.COMMENTATOR, models.LinkRoleChoices.EDITOR]
),
)
],
)
comment = factories.CommentFactory(document=document, content="test", user=user)
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.content == "other content"
def test_update_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be allowed to update comments on a document they don't
have comment access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 403
def test_update_comment_authenticated_no_access():
"""
Authenticated users should not be allowed to update comments on a document they don't
have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 403
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_any_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document, content="test")
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.content == "other content"
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_own_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document, content="test", user=user)
client = APIClient()
client.force_login(user)
response = client.put(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/",
{"content": "other content"},
)
assert response.status_code == 200
comment.refresh_from_db()
assert comment.content == "other content"
# Delete comment
def test_delete_comment_anonymous_user_public_document():
"""Anonymous users should not be allowed to delete comments on a public document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.COMMENTATOR
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_delete_comment_anonymous_user_non_accessible_document():
"""Anonymous users should not be allowed to delete comments on a non-accessible document."""
document = factories.DocumentFactory(
link_reach="public", link_role=models.LinkRoleChoices.READER
)
comment = factories.CommentFactory(document=document)
client = APIClient()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 401
def test_delete_comment_authenticated_user_accessible_document_own_comment():
"""Authenticated users should be able to delete comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment = factories.CommentFactory(document=document, user=user)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
def test_delete_comment_authenticated_user_accessible_document_not_own_comment():
"""Authenticated users should not be able to delete comments on an accessible document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTATOR)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_any_comment(role):
"""Authenticated users should be able to delete comments on a document they have access to."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_own_comment(role):
"""Authenticated users should be able to delete comments on a document they have access to."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(document=document, user=user)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 204
def test_delete_comment_authenticated_user_not_enough_access():
"""
Authenticated users should not be able to delete comments on a document they don't
have access to.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
)
comment = factories.CommentFactory(document=document)
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/comments/{comment.id!s}/"
)
assert response.status_code == 403
@@ -36,7 +36,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": document.link_role in ["commentator", "editor"],
"cors_proxy": True,
"descendants": True,
"destroy": False,
@@ -46,8 +45,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
@@ -112,7 +111,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": grand_parent.link_role in ["commentator", "editor"],
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -218,7 +216,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"children_create": document.link_role == "editor",
"children_list": True,
"collaboration_auth": True,
"comment": document.link_role in ["commentator", "editor"],
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -227,8 +224,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -301,7 +298,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"children_create": grand_parent.link_role == "editor",
"children_list": True,
"collaboration_auth": True,
"comment": grand_parent.link_role in ["commentator", "editor"],
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -492,11 +488,10 @@ def test_api_documents_retrieve_authenticated_related_parent():
"ai_transform": access.role != "reader",
"ai_translate": access.role != "reader",
"attachment_upload": access.role != "reader",
"can_edit": access.role not in ["reader", "commentator"],
"can_edit": access.role != "reader",
"children_create": access.role != "reader",
"children_list": True,
"collaboration_auth": True,
"comment": access.role != "reader",
"descendants": True,
"cors_proxy": True,
"destroy": access.role == "owner",
@@ -79,17 +79,16 @@ def test_api_documents_trashbin_format():
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"cors_proxy": True,
"descendants": True,
"cors_proxy": True,
"destroy": True,
"duplicate": True,
"favorite": True,
"invite_owner": True,
"link_configuration": True,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -1,273 +0,0 @@
"""Test the comment model."""
import random
from django.contrib.auth.models import AnonymousUser
import pytest
from core import factories
from core.models import LinkReachChoices, LinkRoleChoices, RoleChoices
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"role,can_comment",
[
(LinkRoleChoices.READER, False),
(LinkRoleChoices.COMMENTATOR, True),
(LinkRoleChoices.EDITOR, True),
],
)
def test_comment_get_abilities_anonymous_user_public_document(role, can_comment):
"""Anonymous users cannot comment on a document."""
document = factories.DocumentFactory(
link_role=role, link_reach=LinkReachChoices.PUBLIC
)
comment = factories.CommentFactory(document=document)
user = AnonymousUser()
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": can_comment,
}
@pytest.mark.parametrize(
"link_reach", [LinkReachChoices.RESTRICTED, LinkReachChoices.AUTHENTICATED]
)
def test_comment_get_abilities_anonymous_user_restricted_document(link_reach):
"""Anonymous users cannot comment on a restricted document."""
document = factories.DocumentFactory(link_reach=link_reach)
comment = factories.CommentFactory(document=document)
user = AnonymousUser()
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": False,
}
@pytest.mark.parametrize(
"link_role,link_reach,can_comment",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED, True),
],
)
def test_comment_get_abilities_user_reader(link_role, link_reach, can_comment):
"""Readers cannot comment on a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.READER)]
)
comment = factories.CommentFactory(document=document)
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": can_comment,
}
@pytest.mark.parametrize(
"link_role,link_reach,can_comment",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC, True),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED, False),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED, False),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED, True),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED, True),
],
)
def test_comment_get_abilities_user_reader_own_comment(
link_role, link_reach, can_comment
):
"""User with reader role on a document has all accesses to its own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.READER)]
)
comment = factories.CommentFactory(
document=document, user=user if can_comment else None
)
assert comment.get_abilities(user) == {
"destroy": can_comment,
"update": can_comment,
"partial_update": can_comment,
"retrieve": can_comment,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_commentator(link_role, link_reach):
"""Commentators can comment on a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role,
link_reach=link_reach,
users=[(user, RoleChoices.COMMENTATOR)],
)
comment = factories.CommentFactory(document=document)
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": True,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_commentator_own_comment(link_role, link_reach):
"""Commentators have all accesses to its own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role,
link_reach=link_reach,
users=[(user, RoleChoices.COMMENTATOR)],
)
comment = factories.CommentFactory(document=document, user=user)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_editor(link_role, link_reach):
"""Editors can comment on a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.EDITOR)]
)
comment = factories.CommentFactory(document=document)
assert comment.get_abilities(user) == {
"destroy": False,
"update": False,
"partial_update": False,
"retrieve": True,
}
@pytest.mark.parametrize(
"link_role,link_reach",
[
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.COMMENTATOR, LinkReachChoices.AUTHENTICATED),
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
],
)
def test_comment_get_abilities_user_editor_own_comment(link_role, link_reach):
"""Editors have all accesses to its own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.EDITOR)]
)
comment = factories.CommentFactory(document=document, user=user)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
def test_comment_get_abilities_user_admin():
"""Admins have all accesses to a comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, RoleChoices.ADMIN)])
comment = factories.CommentFactory(
document=document, user=random.choice([user, None])
)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
def test_comment_get_abilities_user_owner():
"""Owners have all accesses to a comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, RoleChoices.OWNER)])
comment = factories.CommentFactory(
document=document, user=random.choice([user, None])
)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"retrieve": True,
}
@@ -123,7 +123,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
"set_role_to": ["reader", "editor", "administrator", "owner"],
}
@@ -166,7 +166,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_last_on_child(
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
"set_role_to": ["reader", "editor", "administrator", "owner"],
}
@@ -183,7 +183,7 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
"set_role_to": ["reader", "editor", "administrator", "owner"],
}
@@ -200,7 +200,7 @@ def test_models_document_access_get_abilities_for_owner_of_administrator():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
"set_role_to": ["reader", "editor", "administrator", "owner"],
}
@@ -217,7 +217,7 @@ def test_models_document_access_get_abilities_for_owner_of_editor():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
"set_role_to": ["reader", "editor", "administrator", "owner"],
}
@@ -234,7 +234,7 @@ def test_models_document_access_get_abilities_for_owner_of_reader():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator", "owner"],
"set_role_to": ["reader", "editor", "administrator", "owner"],
}
@@ -271,7 +271,7 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator"],
"set_role_to": ["reader", "editor", "administrator"],
}
@@ -288,7 +288,7 @@ def test_models_document_access_get_abilities_for_administrator_of_editor():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator"],
"set_role_to": ["reader", "editor", "administrator"],
}
@@ -305,7 +305,7 @@ def test_models_document_access_get_abilities_for_administrator_of_reader():
"retrieve": True,
"update": True,
"partial_update": True,
"set_role_to": ["reader", "commentator", "editor", "administrator"],
"set_role_to": ["reader", "editor", "administrator"],
}
+26 -195
View File
@@ -134,13 +134,10 @@ def test_models_documents_soft_delete(depth):
[
(True, "restricted", "reader"),
(True, "restricted", "editor"),
(True, "restricted", "commentator"),
(False, "restricted", "reader"),
(False, "restricted", "editor"),
(False, "restricted", "commentator"),
(False, "authenticated", "reader"),
(False, "authenticated", "editor"),
(False, "authenticated", "commentator"),
],
)
def test_models_documents_get_abilities_forbidden(
@@ -167,7 +164,6 @@ def test_models_documents_get_abilities_forbidden(
"destroy": False,
"duplicate": False,
"favorite": False,
"comment": False,
"invite_owner": False,
"mask": False,
"media_auth": False,
@@ -175,8 +171,8 @@ def test_models_documents_get_abilities_forbidden(
"move": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"partial_update": False,
@@ -226,7 +222,6 @@ def test_models_documents_get_abilities_reader(
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": False,
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -235,77 +230,8 @@ def test_models_documents_get_abilities_reader(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,
"retrieve": True,
"tree": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
}
nb_queries = 1 if is_authenticated else 0
with django_assert_num_queries(nb_queries):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(
value is False
for key, value in document.get_abilities(user).items()
if key not in ["link_select_options", "ancestors_links_definition"]
)
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_documents_get_abilities_commentator(
is_authenticated, reach, django_assert_num_queries
):
"""
Check abilities returned for a document giving commentator role to link holders
i.e anonymous users or authenticated users who have no specific role on the document.
"""
document = factories.DocumentFactory(link_reach=reach, link_role="commentator")
user = factories.UserFactory() if is_authenticated else AnonymousUser()
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": is_authenticated,
@@ -361,7 +287,6 @@ def test_models_documents_get_abilities_editor(
"children_create": is_authenticated,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -370,8 +295,8 @@ def test_models_documents_get_abilities_editor(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": is_authenticated,
@@ -416,7 +341,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": True,
@@ -425,8 +349,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"invite_owner": True,
"link_configuration": True,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -468,7 +392,6 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -477,8 +400,8 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"invite_owner": False,
"link_configuration": True,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -523,7 +446,6 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -532,8 +454,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -585,8 +507,6 @@ def test_models_documents_get_abilities_reader_user(
"children_create": access_from_link,
"children_list": True,
"collaboration_auth": True,
"comment": document.link_reach != "restricted"
and document.link_role in ["commentator", "editor"],
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -595,72 +515,8 @@ def test_models_documents_get_abilities_reader_user(
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": access_from_link,
"restore": False,
"retrieve": True,
"tree": True,
"update": access_from_link,
"versions_destroy": False,
"versions_list": True,
"versions_retrieve": True,
}
with override_settings(AI_ALLOW_REACH_FROM=ai_access_setting):
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(
value is False
for key, value in document.get_abilities(user).items()
if key not in ["link_select_options", "ancestors_links_definition"]
)
@pytest.mark.parametrize("ai_access_setting", ["public", "authenticated", "restricted"])
def test_models_documents_get_abilities_commentator_user(
ai_access_setting, django_assert_num_queries
):
"""Check abilities returned for the commentator of a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "commentator")])
access_from_link = (
document.link_reach != "restricted" and document.link_role == "editor"
)
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
"can_edit": access_from_link,
"children_create": access_from_link,
"children_list": True,
"collaboration_auth": True,
"comment": True,
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": True,
"favorite": True,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -710,7 +566,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"comment": False,
"descendants": True,
"cors_proxy": True,
"destroy": False,
@@ -719,8 +574,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
@@ -1343,14 +1198,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"public",
"reader",
{
"public": ["reader", "commentator", "editor"],
},
),
(
"public",
"commentator",
{
"public": ["commentator", "editor"],
"public": ["reader", "editor"],
},
),
("public", "editor", {"public": ["editor"]}),
@@ -1358,16 +1206,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"authenticated",
"reader",
{
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
},
),
(
"authenticated",
"commentator",
{
"authenticated": ["commentator", "editor"],
"public": ["commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
},
),
(
@@ -1380,17 +1220,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"reader",
{
"restricted": None,
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "commentator", "editor"],
},
),
(
"restricted",
"commentator",
{
"restricted": None,
"authenticated": ["commentator", "editor"],
"public": ["commentator", "editor"],
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
},
),
(
@@ -1407,15 +1238,15 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
"public",
None,
{
"public": ["reader", "commentator", "editor"],
"public": ["reader", "editor"],
},
),
(
None,
"reader",
{
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"],
"authenticated": ["reader", "editor"],
"restricted": None,
},
),
@@ -1423,8 +1254,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
None,
None,
{
"public": ["reader", "commentator", "editor"],
"authenticated": ["reader", "commentator", "editor"],
"public": ["reader", "editor"],
"authenticated": ["reader", "editor"],
"restricted": None,
},
),
+1 -5
View File
@@ -26,11 +26,7 @@ document_related_router.register(
viewsets.InvitationViewset,
basename="invitations",
)
document_related_router.register(
"comments",
viewsets.CommentViewSet,
basename="comments",
)
document_related_router.register(
"ask-for-access",
viewsets.DocumentAskForAccessViewSet,
+68 -72
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Sinedoù"
@@ -70,7 +66,7 @@ msgstr "Doare korf"
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -229,8 +225,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "titl"
@@ -240,161 +236,161 @@ msgstr "bomm"
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Restr"
msgstr "Teul"
#: build/lib/core/models.py:419 core/models.py:419
msgid "Documents"
msgstr "Restroù"
msgstr "Teulioù"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Restr hep titl"
msgstr "Teuliad hep titl"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
msgstr "{name} en deus rannet un teul ganeoc'h!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war an teul da-heul:"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
msgstr "{name} en deus rannet un teul ganeoc'h: {title}"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
msgstr "Roud liamm an teuliad/an implijer"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
msgstr "Roudoù liamm an teuliad/an implijer"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
msgstr "Ur roud liamm a zo dija evit an teul/an implijer."
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Restr muiañ-karet"
msgstr "Teuliad muiañ-karet"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
msgstr "Teuliadoù muiañ-karet"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
msgstr "An teul-mañ a zo un teul muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
msgstr "Liamm teul/implijer"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
msgstr "Liammoù teul/implijer"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
msgstr "An implijer-mañ a zo dija en teul-mañ."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
msgstr "Ar skipailh-mañ a zo dija en teul-mañ."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
msgstr "Goulenn tizhout an teul"
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
msgstr "Goulennoù tizhout an teul"
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
msgstr "An implijer en deus goulennet tizhout an teul-mañ."
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
msgstr "{name} en defe c'hoant da dizhout an teul-mañ!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
msgstr "{name} en defe c'hoant da dizhout an teul da-heul:"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
msgstr "{name} en defe c'hoant da dizhout an teul: {title}"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
msgstr "Pedadenn d'un teul"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
msgstr "Pedadennoù d'un teul"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
@@ -411,7 +407,7 @@ msgstr "Digeriñ"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war an teulioù e skipailh. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Ersteller bin ich"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorit"
@@ -70,7 +66,7 @@ msgstr "Typ"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -229,8 +225,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "Titel"
@@ -246,155 +242,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, 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:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
@@ -70,7 +66,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -229,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr ""
@@ -246,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Yo soy el creador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorito"
@@ -70,7 +66,7 @@ msgstr "Tipo de Cuerpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -229,8 +225,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "título"
@@ -246,155 +242,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Ya existe una traza de enlace para este documento/usuario."
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Je suis l'auteur"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoris"
@@ -70,7 +66,7 @@ msgstr "Type de corps"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -229,8 +225,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "titre"
@@ -246,155 +242,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, 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:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ce document est déjà un favori de cet utilisateur."
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Il creatore sono io"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Preferiti"
@@ -70,7 +66,7 @@ msgstr ""
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -229,8 +225,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "titolo"
@@ -246,155 +242,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, 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:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Ik ben Eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriete"
@@ -70,7 +66,7 @@ msgstr "Text type"
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -229,8 +225,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "titel"
@@ -246,155 +242,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, 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:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Een url bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Eu sou o criador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorito"
@@ -70,7 +66,7 @@ msgstr "Tipo de corpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -229,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr ""
@@ -246,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Ustvaril sem jaz"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Priljubljena"
@@ -70,7 +66,7 @@ msgstr "Vrsta telesa"
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -229,8 +225,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "naslov"
@@ -246,155 +242,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, 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:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "Skaparen är jag"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriter"
@@ -70,7 +66,7 @@ msgstr ""
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -229,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr ""
@@ -246,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
@@ -70,7 +66,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -229,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr ""
@@ -246,155 +242,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""
+43 -47
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -43,10 +43,6 @@ msgid "Creator is me"
msgstr "创建者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "收藏"
@@ -70,7 +66,7 @@ msgstr "正文类型"
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -229,8 +225,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
msgid "title"
msgstr "标题"
@@ -246,155 +242,155 @@ msgstr "文档"
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:856 core/models.py:856
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:860 core/models.py:860
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1038 core/models.py:1038
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1262 core/models.py:1262
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1290 core/models.py:1290
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1357 core/models.py:1357
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1363 core/models.py:1363
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1440 core/models.py:1440
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1480 core/models.py:1480
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.5.0"
version = "3.4.2"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await expect(
page.locator('header').first().locator('h1').getByText('Docs'),
page.locator('header').first().locator('h2').getByText('Docs'),
).toBeVisible();
await page.goto('unknown-page404');
});
@@ -23,7 +23,7 @@ const saveStorageState = async (
page.locator('header').first().getByRole('button', {
name: 'Logout',
}),
).toBeVisible({ timeout: 10000 });
).toBeVisible();
await page.context().storageState({
path: storageState as string,
@@ -43,9 +43,7 @@ test.describe('Config', () => {
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
@@ -22,7 +22,7 @@ test.describe('Doc Create', () => {
);
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
await header.locator('h2').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
@@ -0,0 +1,74 @@
import { expect, test } from '@playwright/test';
test.describe('Doc Editor - Heading Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should filter heading options progressively (h1 -> h2 -> h3)', async ({
page,
}) => {
await page.getByRole('button', { name: 'Nouveau doc' }).click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'domcontentloaded',
});
const input = page.getByLabel('doc title input');
await input.fill('heading-accessibility-test');
await input.blur();
const editor = page.locator('.ProseMirror');
await editor.click();
await page.keyboard.type('/');
await expect(page.getByText('Titre 1')).toBeVisible();
await expect(page.getByText('Titre 2')).toBeHidden();
await expect(page.getByText('Titre 3')).toBeHidden();
await page.getByText('Titre 1').click();
await page.keyboard.type('Main Title');
await page.keyboard.press('Enter');
await editor.click();
await page.keyboard.type('/');
await expect(page.getByText('Titre 1')).toBeHidden();
await expect(page.getByText('Titre 2')).toBeVisible();
await expect(page.getByText('Titre 3')).toBeHidden();
await page.getByText('Titre 2').click();
await page.keyboard.type('Sub Title');
await page.keyboard.press('Enter');
await editor.click();
await page.keyboard.type('/');
await expect(page.getByText('Titre 1')).toBeHidden();
await expect(page.getByText('Titre 2')).toBeVisible();
await expect(page.getByText('Titre 3')).toBeVisible();
await page.getByText('Titre 3').click();
await page.keyboard.type('Sub Sub Title');
await page.keyboard.press('Enter');
await editor.click();
await page.keyboard.type('/');
await expect(page.getByText('Titre 1')).toBeHidden();
await expect(page.getByText('Titre 2')).toBeHidden();
await expect(page.getByText('Titre 3')).toBeVisible();
await page.getByText('Titre 3').click();
await page.keyboard.type('Another Sub Sub Title');
await page.keyboard.press('Enter');
await editor.click();
await page.keyboard.type('/');
await expect(page.getByText('Titre 1')).toBeHidden();
await expect(page.getByText('Titre 2')).toBeHidden();
await expect(page.getByText('Titre 3')).toBeVisible();
});
});
@@ -93,7 +93,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
// When the visibility is changed, the ws should close the connection (backend signal)
const wsClosePromise = webSocket.waitForEvent('close');
@@ -272,9 +272,7 @@ test.describe('Doc Editor', () => {
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
@@ -286,11 +284,6 @@ test.describe('Doc Editor', () => {
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
await expect(image).toHaveAttribute('role', 'presentation');
await expect(image).toHaveAttribute('alt', '');
await expect(image).toHaveAttribute('tabindex', '-1');
await expect(image).toHaveAttribute('aria-hidden', 'true');
});
test('it checks the AI buttons', async ({ page, browserName }) => {
@@ -568,7 +561,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-visibility').click();
await page.getByLabel('Visibility', { exact: true }).click();
await page
.getByRole('menuitem', {
@@ -580,7 +573,7 @@ test.describe('Doc Editor', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Visibility mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
// Close the modal
@@ -662,7 +655,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Visibility mode').click();
await page.getByRole('menuitem', { name: 'Reading' }).click();
// Close the modal
@@ -4,13 +4,7 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import pdf from 'pdf-parse';
import {
TestLanguage,
createDoc,
randomName,
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
@@ -122,9 +116,7 @@ test.describe('Doc Export', () => {
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();
const image = page.getByRole('img', { name: 'test.svg' });
await expect(image).toBeVisible();
@@ -184,9 +176,7 @@ test.describe('Doc Export', () => {
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();
const image = page.getByRole('img', { name: 'test.svg' });
await expect(image).toBeVisible();
@@ -423,73 +413,6 @@ test.describe('Doc Export', () => {
expect(pdfData.text).toContain('Column 3');
});
test('it injects the correct language attribute into PDF export', async ({
page,
browserName,
}) => {
await waitForLanguageSwitch(page, TestLanguage.French);
// Wait for the page to be ready after language switch
await page.waitForLoadState('domcontentloaded');
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
const randomDocFrench = randomName(
'doc-language-export-french',
browserName,
1,
)[0];
await page
.getByRole('button', {
name: 'Nouveau doc',
})
.click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'domcontentloaded',
});
const input = page.getByLabel('doc title input');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await input.click();
await input.fill(randomDocFrench);
await input.blur();
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.fill('Contenu de test pour export en français');
await page
.getByRole('button', {
name: 'download',
exact: true,
})
.click();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page
.getByRole('button', {
name: 'Télécharger',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfString = pdfBuffer.toString('latin1');
expect(pdfString).toContain('/Lang (fr)');
});
test('it exports the doc with interlinking', async ({
page,
browserName,
@@ -8,9 +8,9 @@ test.describe('Doc grid dnd', () => {
await page.goto('/');
const header = page.locator('header').first();
await createDoc(page, 'Draggable doc', browserName, 1);
await header.locator('h1').getByText('Docs').click();
await header.locator('h2').getByText('Docs').click();
await createDoc(page, 'Droppable doc', browserName, 1);
await header.locator('h1').getByText('Docs').click();
await header.locator('h2').getByText('Docs').click();
const response = await page.waitForResponse(
(response) =>
@@ -119,7 +119,7 @@ test.describe('Document grid item options', () => {
await page.getByText('push_pin').click();
// Check is pinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
await expect(row.getByLabel('Pin document icon')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -128,7 +128,7 @@ test.describe('Document grid item options', () => {
await page.getByText('Unpin').click();
// Check is unpinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -227,18 +227,18 @@ test.describe('Documents filters', () => {
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveAttribute('aria-current', 'page');
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
await expect(myDocs).toBeVisible();
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(myDocs).not.toHaveAttribute('aria-current');
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
await expect(sharedWithMe).toBeVisible();
await expect(sharedWithMe).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
await allDocs.click();
@@ -30,7 +30,7 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-visibility').click();
await page.getByLabel('Visibility', { exact: true }).click();
await page
.getByRole('menuitem', {
@@ -409,7 +409,7 @@ test.describe('Doc Header', () => {
const row = await getGridRow(page, docTitle);
// Check is pinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
await expect(row.getByLabel('Pin document icon')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
await page.goto('/');
// Check is unpinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -244,7 +244,9 @@ test.describe('Document create member: Multiple login', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);
@@ -269,7 +271,9 @@ test.describe('Document create member: Multiple login', () => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
timeout: 10000,
});
@@ -330,7 +334,9 @@ test.describe('Document create member: Multiple login', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
timeout: 10000,
});
@@ -26,7 +26,10 @@ test.describe('Document search', () => {
);
await verifyDocName(page, doc2Title);
await page.goto('/');
await page.getByTestId('search-docs-button').click();
await page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' })
.click();
await expect(
page.getByRole('img', { name: 'No active search' }),
@@ -101,7 +104,9 @@ test.describe('Document search', () => {
browserName,
}) => {
// Doc grid filters are not visible
const searchButton = page.getByTestId('search-docs-button');
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search', exact: true });
const filters = page.getByTestId('doc-search-filters');
@@ -165,7 +170,9 @@ test.describe('Document search', () => {
1,
);
const searchButton = page.getByTestId('search-docs-button');
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' });
await searchButton.click();
await page.getByRole('combobox', { name: 'Quick search input' }).click();
@@ -25,7 +25,7 @@ test.describe('Doc Tree', () => {
1,
);
await verifyDocName(page, titleParent);
const addButton = page.getByTestId('new-doc-button');
const addButton = page.getByRole('button', { name: 'New doc' });
const docTree = page.getByTestId('doc-tree');
await expect(addButton).toBeVisible();
@@ -63,7 +63,7 @@ test.describe('Doc Tree', () => {
test('check the reorder of sub pages', async ({ page, browserName }) => {
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByTestId('new-doc-button');
const addButton = page.getByRole('button', { name: 'New doc' });
await expect(addButton).toBeVisible();
const docTree = page.getByTestId('doc-tree');
@@ -201,7 +201,7 @@ test.describe('Doc Tree', () => {
).not.toHaveText(docChild);
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
await header.locator('h2').getByText('Docs').click();
await expect(page.getByText(docChild)).toBeVisible();
});
@@ -259,10 +259,6 @@ test.describe('Doc Tree: Inheritance', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('A child inherit from the parent', async ({ page, browserName }) => {
// test.slow() to extend timeout since this scenario chains Keycloak login + redirects,
// doc creation/navigation and async doc-tree loading (/documents/:id/tree), which can exceed 30s (especially in CI).
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -275,7 +271,7 @@ test.describe('Doc Tree: Inheritance', () => {
await verifyDocName(page, docParent);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
@@ -311,7 +307,6 @@ test.describe('Doc Tree: Inheritance', () => {
await expect(page.locator('h2').getByText(docChild)).toBeVisible();
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible({ timeout: 10000 });
await expect(docTree.getByText(docParent)).toBeVisible();
});
});
@@ -41,7 +41,7 @@ test.describe('Doc Visibility', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await expect(selectVisibility.getByText('Private')).toBeVisible();
@@ -51,13 +51,13 @@ test.describe('Doc Visibility', () => {
await selectVisibility.click();
await page.getByLabel('Connected').click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await selectVisibility.click();
await page.getByLabel('Public', { exact: true }).click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
});
});
@@ -122,7 +122,9 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
timeout: 10000,
});
@@ -176,7 +178,9 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);
@@ -205,7 +209,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
@@ -218,8 +222,8 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await page.getByLabel('Visibility mode').click();
await page
.getByRole('menuitem', {
name: 'Reading',
@@ -242,8 +246,8 @@ test.describe('Doc Visibility: Public', () => {
cardContainer.getByText('Public document', { exact: true }),
).toBeVisible();
await expect(page.getByTestId('search-docs-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
const urlDoc = page.url();
@@ -258,8 +262,8 @@ test.describe('Doc Visibility: Public', () => {
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await expect(page.getByTestId('search-docs-button')).toBeHidden();
await expect(page.getByTestId('new-doc-button')).toBeHidden();
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
const card = page.getByLabel('It is the card information');
await expect(card).toBeVisible();
@@ -289,7 +293,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
@@ -302,7 +306,7 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Visibility mode').click();
await page.getByLabel('Editing').click();
await expect(
@@ -358,7 +362,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -410,7 +414,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -451,7 +455,9 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
timeout: 10000,
});
@@ -495,7 +501,6 @@ test.describe('Doc Visibility: Authenticated', () => {
page,
browserName,
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -509,7 +514,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -522,7 +527,7 @@ test.describe('Doc Visibility: Authenticated', () => {
).toBeVisible();
const urlDoc = page.url();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Visibility mode').click();
await page.getByLabel('Editing').click();
await expect(
@@ -540,17 +545,15 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible({
timeout: 10000,
});
await expect(page.getByText('Link Copied !')).toBeVisible();
});
});
@@ -12,8 +12,8 @@ test.describe('Header', () => {
const header = page.locator('header').first();
await expect(header.getByTestId('header-logo-link')).toBeVisible();
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'font-family',
/Roboto/i,
);
@@ -37,8 +37,8 @@ test.describe('Header', () => {
const header = page.locator('header').first();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'font-family',
/Marianne/i,
);
@@ -106,7 +106,7 @@ test.describe('Header mobile', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Open the header menu')).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.getByRole('link', { name: 'Docs Logo' })).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
@@ -15,13 +15,10 @@ test.describe('Home page', () => {
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
const languageButton = page.getByRole('button', {
name: /Language|Select language/,
});
await expect(languageButton).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
@@ -68,31 +65,20 @@ test.describe('Home page', () => {
await page.goto('/docs/');
// Wait for the page to be fully loaded and responsive store to be initialized
await page.waitForLoadState('domcontentloaded');
// Wait a bit more for the responsive store to be initialized
await page.waitForTimeout(500);
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
// Check for language picker - it should be visible on desktop
// Use a more flexible selector that works with both Header and HomeHeader
const languageButton = page.getByRole('button', {
name: /Language|Select language/,
});
await expect(languageButton).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
await expect(
header.getByRole('button', { name: 'Les services de La Suite numé' }),
).toBeVisible();
await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
@@ -1,17 +1,30 @@
import { expect, test } from '@playwright/test';
import { Page, expect, test } from '@playwright/test';
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
import { createDoc } from './utils-common';
test.describe.serial('Language', () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test.describe('Language', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
});
test.afterEach(async ({ page }) => {
// Switch back to English - important for other tests to run as expected
await waitForLanguageSwitch(page, TestLanguage.English);
});
test('checks language switching', async ({ page }) => {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
// initial language should be english
await expect(
@@ -23,57 +36,17 @@ test.describe('Language', () => {
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(
header.getByRole('button').getByText('Français'),
).toBeVisible();
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
// Switch to German using the utility function for consistency
await waitForLanguageSwitch(page, TestLanguage.German);
await header.getByRole('button').getByText('Français').click();
await page.getByLabel('Deutsch').click();
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
await expect(page.getByLabel('Abmelden')).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await languagePicker.click();
await expect(page.locator('[role="menu"]')).toBeVisible();
const menuItems = page.getByRole('menuitem');
await expect(menuItems.first()).toBeVisible();
await menuItems.first().click();
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(languagePicker).toContainText('English');
});
test('can switch language using only keyboard', async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
const languagePicker = page.getByRole('button', {
name: /select language/i,
});
await expect(languagePicker).toBeVisible();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
const menu = page.getByRole('menu');
await expect(menu).toBeVisible();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us');
});
test('checks that backend uses the same language as the frontend', async ({
@@ -121,3 +94,48 @@ test.describe('Language', () => {
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
});
});
// language helper
export const TestLanguage = {
English: {
label: 'English',
expectedLocale: ['en-us'],
},
French: {
label: 'Français',
expectedLocale: ['fr-fr'],
},
German: {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
} as const;
type TestLanguageKey = keyof typeof TestLanguage;
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
const isAlreadyTargetLanguage = await languagePicker
.innerText()
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
if (isAlreadyTargetLanguage) {
return;
}
await languagePicker.click();
const responsePromise = page.waitForResponse(
(resp) =>
resp.url().includes('/user') && resp.request().method() === 'PATCH',
);
await page.getByLabel(lang.label).click();
const resolvedResponsePromise = await responsePromise;
const responseData = await resolvedResponsePromise.json();
expect(lang.expectedLocale).toContain(responseData.language);
}
@@ -8,8 +8,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')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
await expect(page.getByRole('button', { name: 'house' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
});
});
@@ -27,11 +27,9 @@ test.describe('Left panel mobile', () => {
await expect(page.getByTestId('left-panel-mobile')).not.toBeInViewport();
const header = page.locator('header').first();
const homeButton = page.getByTestId('home-button');
const newDocButton = page.getByTestId('new-doc-button');
const languageButton = page.getByRole('button', {
name: 'Select language',
});
const homeButton = page.getByRole('button', { name: 'house' });
const newDocButton = page.getByRole('button', { name: 'New doc' });
const languageButton = page.getByRole('button', { name: /Language/ });
const logoutButton = page.getByRole('button', { name: 'Logout' });
await expect(homeButton).not.toBeInViewport();
@@ -154,7 +154,7 @@ export const goToGridDoc = async (
{ nthRow = 1, title }: GoToGridDocOptions = {},
) => {
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
await header.locator('h2').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
@@ -273,52 +273,3 @@ export const expectLoginPage = async (page: Page) =>
).toBeVisible({
timeout: 10000,
});
// language helper
export const TestLanguage = {
English: {
label: 'English',
expectedLocale: ['en-us'],
},
French: {
label: 'Français',
expectedLocale: ['fr-fr'],
},
German: {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
} as const;
type TestLanguageKey = keyof typeof TestLanguage;
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
await page.route('**/api/v1.0/users/**', async (route, request) => {
if (request.method().includes('PATCH')) {
await route.fulfill({
json: {
language: lang.expectedLocale[0],
},
});
} else {
await route.continue();
}
});
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
const isAlreadyTargetLanguage = await languagePicker
.innerText()
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
if (isAlreadyTargetLanguage) {
return;
}
await languagePicker.click();
await page.getByLabel(lang.label).click();
}
@@ -45,7 +45,7 @@ export const updateShareLink = async (
linkReach: LinkReach,
linkRole?: LinkRole | null,
) => {
await page.getByTestId('doc-visibility').click();
await page.getByRole('button', { name: 'Visibility', exact: true }).click();
await page.getByRole('menuitem', { name: linkReach }).click();
const visibilityUpdatedText = page
@@ -55,7 +55,9 @@ export const updateShareLink = async (
await expect(visibilityUpdatedText).toBeVisible();
if (linkRole) {
await page.getByTestId('doc-access-mode').click();
await page
.getByRole('button', { name: 'Visibility mode', exact: true })
.click();
await page.getByRole('menuitem', { name: linkRole }).click();
await expect(visibilityUpdatedText).toBeVisible();
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.5.0",
"version": "3.4.2",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.54.2",
"@playwright/test": "1.54.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-config-impress": "*",
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.5.0",
"version": "3.4.2",
"private": true,
"scripts": {
"dev": "next dev",
@@ -29,12 +29,12 @@
"@emoji-mart/react": "1.1.1",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.11.0",
"@gouvfr-lasuite/ui-kit": "0.10.0",
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.2.1",
"@openfun/cunningham-react": "3.2.0",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "10.2.0",
"@tanstack/react-query": "5.84.1",
"@sentry/nextjs": "9.42.0",
"@tanstack/react-query": "5.83.0",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -46,8 +46,8 @@
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.1",
"next": "15.4.6",
"posthog-js": "1.258.6",
"next": "15.4.4",
"posthog-js": "1.258.2",
"react": "*",
"react-aria-components": "1.11.0",
"react-dom": "*",
@@ -58,22 +58,22 @@
"use-debounce": "10.0.5",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.7"
"zustand": "5.0.6"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.84.1",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.6.4",
"@tanstack/react-query-devtools": "5.83.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/jest": "30.0.0",
"@types/lodash": "4.17.20",
"@types/luxon": "3.7.1",
"@types/luxon": "3.6.2",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"cross-env": "10.0.0",
"cross-env": "7.0.3",
"dotenv": "17.2.1",
"eslint-config-impress": "*",
"fetch-mock": "9.11.0",
@@ -81,11 +81,11 @@
"jest-environment-jsdom": "30.0.5",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.23.0",
"stylelint-config-standard": "39.0.0",
"stylelint": "16.22.0",
"stylelint-config-standard": "38.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"webpack": "5.101.0",
"webpack": "5.100.2",
"workbox-webpack-plugin": "7.1.0"
}
}
@@ -33,13 +33,6 @@ const StyledButton = styled(Button)<StyledButtonProps>`
font-size: 0.938rem;
padding: 0;
${({ $css }) => $css};
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: 2px;
border-radius: 4px;
transition: none;
}
`;
export interface DropButtonProps {
@@ -48,7 +41,6 @@ export interface DropButtonProps {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
label?: string;
testId?: string;
}
export const DropButton = ({
@@ -58,7 +50,6 @@ export const DropButton = ({
onOpenChange,
children,
label,
testId,
}: PropsWithChildren<DropButtonProps>) => {
const { themeTokens } = useCunninghamTheme();
const font = themeTokens['font']?.['families']['base'];
@@ -81,7 +72,6 @@ export const DropButton = ({
ref={triggerRef}
onPress={() => onOpenChangeHandler(true)}
aria-label={label}
data-testid={testId}
$css={css`
font-family: ${font};
${buttonCss};
@@ -1,19 +1,10 @@
import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
import {
Fragment,
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { Fragment, PropsWithChildren, useRef, useState } from 'react';
import { css } from 'styled-components';
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
export type DropdownMenuOption = {
icon?: string;
label: string;
@@ -38,7 +29,6 @@ export type DropdownMenuProps = {
topMessage?: string;
selectedValues?: string[];
afterOpenChange?: (isOpen: boolean) => void;
testId?: string;
};
export const DropdownMenu = ({
@@ -53,44 +43,15 @@ export const DropdownMenu = ({
topMessage,
afterOpenChange,
selectedValues,
testId,
}: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(opened ?? false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const blockButtonRef = useRef<HTMLDivElement>(null);
const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]);
const onOpenChange = useCallback(
(isOpen: boolean) => {
setIsOpen(isOpen);
setFocusedIndex(-1);
afterOpenChange?.(isOpen);
},
[afterOpenChange],
);
useDropdownKeyboardNav({
isOpen,
focusedIndex,
options,
menuItemRefs,
setFocusedIndex,
onOpenChange,
});
// Focus selected menu item when menu opens
useEffect(() => {
if (isOpen && menuItemRefs.current.length > 0) {
const selectedIndex = options.findIndex((option) => option.isSelected);
if (selectedIndex !== -1) {
setFocusedIndex(selectedIndex);
setTimeout(() => {
menuItemRefs.current[selectedIndex]?.focus();
}, 0);
}
}
}, [isOpen, options]);
const onOpenChange = (isOpen: boolean) => {
setIsOpen(isOpen);
afterOpenChange?.(isOpen);
};
if (disabled) {
return children;
@@ -102,7 +63,6 @@ export const DropdownMenu = ({
onOpenChange={onOpenChange}
label={label}
buttonCss={buttonCss}
testId={testId}
button={
showArrow ? (
<Box
@@ -135,7 +95,6 @@ export const DropdownMenu = ({
$maxWidth="320px"
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
role="menu"
aria-label={label}
>
{topMessage && (
<Text
@@ -156,20 +115,14 @@ export const DropdownMenu = ({
return;
}
const isDisabled = option.disabled !== undefined && option.disabled;
const isFocused = index === focusedIndex;
return (
<Fragment key={option.label}>
<BoxButton
ref={(el) => {
menuItemRefs.current[index] = el;
}}
role="menuitem"
aria-label={option.label}
data-testid={option.testId}
$direction="row"
disabled={isDisabled}
$hasTransition={false}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -205,19 +158,6 @@ export const DropdownMenu = ({
&:hover {
background-color: var(--c--theme--colors--greyscale-050);
}
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
}
${isFocused &&
css`
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
`}
`}
>
<Box
@@ -1,88 +0,0 @@
import { RefObject, useEffect } from 'react';
import { DropdownMenuOption } from '../DropdownMenu';
type UseDropdownKeyboardNavProps = {
isOpen: boolean;
focusedIndex: number;
options: DropdownMenuOption[];
menuItemRefs: RefObject<(HTMLDivElement | null)[]>;
setFocusedIndex: (index: number) => void;
onOpenChange: (isOpen: boolean) => void;
};
export const useDropdownKeyboardNav = ({
isOpen,
focusedIndex,
options,
menuItemRefs,
setFocusedIndex,
onOpenChange,
}: UseDropdownKeyboardNavProps) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!isOpen) {
return;
}
const enabledIndices = options
.map((option, index) =>
option.show !== false && !option.disabled ? index : -1,
)
.filter((index) => index !== -1);
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
const nextIndex =
focusedIndex < enabledIndices.length - 1 ? focusedIndex + 1 : 0;
const nextEnabledIndex = enabledIndices[nextIndex];
setFocusedIndex(nextIndex);
menuItemRefs.current[nextEnabledIndex]?.focus();
break;
case 'ArrowUp':
event.preventDefault();
const prevIndex =
focusedIndex > 0 ? focusedIndex - 1 : enabledIndices.length - 1;
const prevEnabledIndex = enabledIndices[prevIndex];
setFocusedIndex(prevIndex);
menuItemRefs.current[prevEnabledIndex]?.focus();
break;
case 'Enter':
case ' ':
event.preventDefault();
if (focusedIndex >= 0 && focusedIndex < enabledIndices.length) {
const selectedOptionIndex = enabledIndices[focusedIndex];
const selectedOption = options[selectedOptionIndex];
if (selectedOption && selectedOption.callback) {
onOpenChange(false);
void selectedOption.callback();
}
}
break;
case 'Escape':
event.preventDefault();
onOpenChange(false);
break;
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [
isOpen,
focusedIndex,
options,
menuItemRefs,
setFocusedIndex,
onOpenChange,
]);
};
@@ -1,12 +1,9 @@
import { css } from 'styled-components';
import { Box } from '../Box';
import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu';
import { Icon } from '../Icon';
import { Text } from '../Text';
import {
DropdownMenu,
DropdownMenuOption,
} from '../dropdown-menu/DropdownMenu';
export type FilterDropdownProps = {
options: DropdownMenuOption[];
@@ -3,7 +3,7 @@ export * from './Box';
export * from './BoxButton';
export * from './Card';
export * from './DropButton';
export * from './dropdown-menu/DropdownMenu';
export * from './DropdownMenu';
export * from './quick-search';
export * from './Icon';
export * from './InfiniteScroll';
@@ -46,9 +46,7 @@ export const QuickSearchInput = ({
$gap={spacingsTokens['2xs']}
$padding={{ horizontal: 'base', vertical: 'sm' }}
>
{!loading && (
<Icon iconName="search" $variation="600" aria-hidden="true" />
)}
{!loading && <Icon iconName="search" $variation="600" />}
{loading && (
<div>
<Loader size="small" />
@@ -33,11 +33,7 @@ import { randomColor } from '../utils';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import {
AccessibleImageBlock,
CalloutBlock,
DividerBlock,
} from './custom-blocks';
import { CalloutBlock, DividerBlock } from './custom-blocks';
import {
InterlinkingLinkInlineContent,
InterlinkingSearchInlineContent,
@@ -54,7 +50,6 @@ const baseBlockNoteSchema = withPageBreak(
...defaultBlockSpecs,
callout: CalloutBlock,
divider: DividerBlock,
image: AccessibleImageBlock,
},
inlineContentSpecs: {
...defaultInlineContentSpecs,
@@ -9,6 +9,7 @@ import {
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHeadingAccessibilityFilter } from '../hook';
import {
DocsBlockSchema,
DocsInlineContentSchema,
@@ -34,6 +35,7 @@ export const BlockNoteSuggestionMenu = () => {
const { t } = useTranslation();
const basicBlocksName = useDictionary().slash_menu.page_break.group;
const getInterlinkingMenuItems = useGetInterlinkingMenuItems();
const { filterHeadingItemsByAccessibility } = useHeadingAccessibilityFilter();
const getSlashMenuItems = useMemo(() => {
// We insert it after the "Code Block" item to have the interlinking block displayed after the basic blocks
@@ -47,11 +49,16 @@ export const BlockNoteSuggestionMenu = () => {
...defaultMenu.slice(index + 1),
];
const filteredMenuItems = filterHeadingItemsByAccessibility(
newSlashMenuItems,
editor,
);
return async (query: string) =>
Promise.resolve(
filterSuggestionItems(
combineByGroup(
newSlashMenuItems,
filteredMenuItems,
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
getMultiColumnSlashMenuItems?.(editor) || [],
getPageBreakReactSlashMenuItems(editor),
@@ -60,7 +67,13 @@ export const BlockNoteSuggestionMenu = () => {
query,
),
);
}, [basicBlocksName, editor, getInterlinkingMenuItems, t]);
}, [
basicBlocksName,
editor,
getInterlinkingMenuItems,
t,
filterHeadingItemsByAccessibility,
]);
return (
<SuggestionMenuController
@@ -119,11 +119,7 @@ export const DocVersionEditor = ({
causes={error.cause}
icon={
error.status === 502 ? (
<Text
className="material-icons"
$theme="danger"
aria-hidden={true}
>
<Text className="material-icons" $theme="danger">
wifi_off
</Text>
) : undefined
@@ -1,43 +0,0 @@
import {
BlockFromConfig,
BlockNoteEditor,
BlockSchemaWithBlock,
InlineContentSchema,
StyleSchema,
createBlockSpec,
imageBlockConfig,
imageParse,
imageRender,
imageToExternalHTML,
} from '@blocknote/core';
type ImageBlockConfig = typeof imageBlockConfig;
export const accessibleImageRender = (
block: BlockFromConfig<ImageBlockConfig, InlineContentSchema, StyleSchema>,
editor: BlockNoteEditor<
BlockSchemaWithBlock<ImageBlockConfig['type'], ImageBlockConfig>,
InlineContentSchema,
StyleSchema
>,
) => {
const imageRenderComputed = imageRender(block, editor);
const dom = imageRenderComputed.dom;
const imgSelector = dom.querySelector('img');
imgSelector?.setAttribute('alt', '');
imgSelector?.setAttribute('role', 'presentation');
imgSelector?.setAttribute('aria-hidden', 'true');
imgSelector?.setAttribute('tabindex', '-1');
return {
...imageRenderComputed,
dom,
};
};
export const AccessibleImageBlock = createBlockSpec(imageBlockConfig, {
render: accessibleImageRender,
parse: imageParse,
toExternalHTML: imageToExternalHTML,
});
@@ -1,3 +1,2 @@
export * from './AccessibleImageBlock';
export * from './CalloutBlock';
export * from './DividerBlock';
@@ -55,7 +55,6 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
return (
<StyledLink
href={url}
draggable="false"
$css={css`
display: inline;
padding: 0.1rem 0.4rem;
@@ -2,3 +2,4 @@ export * from './useHeadings';
export * from './useSaveDoc';
export * from './useShortcuts';
export * from './useUploadFile';
export * from './useHeadingAccessibilityFilter';
@@ -0,0 +1,87 @@
import { getDefaultReactSlashMenuItems } from '@blocknote/react';
import { DocsBlockNoteEditor } from '../types';
export const useHeadingAccessibilityFilter = () => {
// function to extract heading level from menu item
const getHeadingLevel = (
item: ReturnType<typeof getDefaultReactSlashMenuItems>[0],
): number => {
const title = item.title?.toLowerCase() || '';
const aliases = item.aliases || [];
const HEADING_2 = 'heading 2';
const HEADING_3 = 'heading 3';
const TITLE_2 = 'titre 2';
const TITLE_3 = 'titre 3';
if (
title.includes(HEADING_2) ||
title.includes(TITLE_2) ||
aliases.some(
(alias: string) => alias.includes(HEADING_2) || alias.includes(TITLE_2),
)
) {
return 2;
}
if (
title.includes(HEADING_3) ||
title.includes(TITLE_3) ||
aliases.some(
(alias: string) => alias.includes(HEADING_3) || alias.includes(TITLE_3),
)
) {
return 3;
}
return 1;
};
// function to check if item is a heading
const isHeadingItem = (
item: ReturnType<typeof getDefaultReactSlashMenuItems>[0],
): boolean => {
return item.onItemClick?.toString().includes('heading');
};
const filterHeadingItemsByAccessibility = (
items: ReturnType<typeof getDefaultReactSlashMenuItems>,
editor: DocsBlockNoteEditor,
) => {
const existingLevels = editor.document
.filter((block) => block.type === 'heading')
.map((block) => (block.props as { level: number }).level);
const hasH1 = existingLevels.includes(1);
if (existingLevels.length === 0) {
return items.filter(
(item) => !isHeadingItem(item) || getHeadingLevel(item) === 1,
);
}
const maxLevel = Math.max(...existingLevels);
const minLevel = Math.min(...existingLevels);
return items.filter((item) => {
if (!isHeadingItem(item)) {
return true;
}
const headingLevel = getHeadingLevel(item);
// Never allow h1 if one already exists >> accessibility tells that we can only have one h1 per document
if (headingLevel === 1 && hasH1) {
return false;
}
return (
headingLevel === maxLevel ||
headingLevel === maxLevel + 1 ||
(headingLevel === minLevel - 1 && minLevel > 1)
);
});
};
return { filterHeadingItemsByAccessibility };
};
@@ -10,8 +10,7 @@ import {
useToastProvider,
} from '@openfun/cunningham-react';
import { DocumentProps, pdf } from '@react-pdf/renderer';
import i18next from 'i18next';
import { cloneElement, isValidElement, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -93,15 +92,10 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
});
const rawPdfDocument = (await exporter.toReactPDFDocument(
const pdfDocument = (await exporter.toReactPDFDocument(
exportDocument,
)) as React.ReactElement<DocumentProps>;
// Inject language for screen reader support
const pdfDocument = isValidElement(rawPdfDocument)
? cloneElement(rawPdfDocument, { language: i18next.language })
: rawPdfDocument;
blobExport = await pdf(pdfDocument).toBlob();
} else {
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
@@ -43,9 +43,9 @@ export type DocsExporterPDF = Exporter<
>;
export type DocsExporterDocx = Exporter<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
NoInfer<DocsBlockSchema>,
NoInfer<DocsInlineContentSchema>,
NoInfer<DocsStyleSchema>,
Promise<Paragraph[] | Paragraph | Table> | Paragraph[] | Paragraph | Table,
ParagraphChild,
IRunPropertiesOptions,
@@ -52,17 +52,14 @@ export const SimpleDocItem = ({
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
`}
$padding={`${spacingsTokens['3xs']} 0`}
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
>
{isPinned ? (
<PinnedDocumentIcon
aria-hidden="true"
aria-label={t('Pin document icon')}
color={colorsTokens['primary-500']}
/>
) : (
<SimpleFileIcon
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
/>
@@ -39,11 +39,7 @@ export const DocShareModalFooter = ({
fullWidth={false}
onClick={copyDocLink}
color="tertiary"
icon={
<span className="material-icons" aria-hidden={true}>
add_link
</span>
}
icon={<span className="material-icons">add_link</span>}
>
{t('Copy link')}
</Button>
@@ -129,8 +129,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
$gap={canManage ? spacingsTokens['3xs'] : spacingsTokens['base']}
>
<DropdownMenu
testId="doc-visibility"
label={t('Document visibility')}
label={t('Visibility')}
arrowCss={css`
color: ${colorsTokens['primary-800']} !important;
`}
@@ -171,7 +170,6 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
<Box $direction="row" $align="center" $gap={spacingsTokens['3xs']}>
{docLinkReach !== LinkReach.RESTRICTED && (
<DropdownMenu
testId="doc-access-mode"
disabled={!canManage}
showArrow={true}
options={linkRoleOptions}
@@ -182,7 +180,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
)
: undefined
}
label={t('Document access mode')}
label={t('Visibility mode')}
>
<Text $weight="initial" $variation="600">
{linkModeTranslations[docLinkRole]}
@@ -1,6 +1,5 @@
import { useModal } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
import {
@@ -76,26 +75,14 @@ export const DocsGridActions = ({
},
];
const documentTitle = doc.title || t('Untitled document');
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
title: documentTitle,
});
return (
<>
<DropdownMenu options={options} label={menuLabel}>
<DropdownMenu options={options}>
<Icon
data-testid={`docs-grid-actions-button-${doc.id}`}
iconName="more_horiz"
$theme="primary"
$variation="600"
aria-label={t('More options')}
$css={css`
cursor: pointer;
&:hover {
opacity: 0.8;
}
`}
/>
</DropdownMenu>
@@ -39,11 +39,7 @@ export const Header = () => {
className="--docs--header"
>
{!isDesktop && <ButtonTogglePanel />}
<StyledLink
href="/"
data-testid="header-logo-link"
aria-label={t('Back to homepage')}
>
<StyledLink href="/">
<Box
$align="center"
$gap={spacingsTokens['3xs']}
@@ -53,12 +49,11 @@ export const Header = () => {
$margin={{ top: 'auto' }}
>
<IconDocs
data-testid="header-icon-docs"
aria-label={t('Docs Logo')}
width={32}
color={colorsTokens['primary-text']}
aria-hidden="true"
/>
<Title headingLevel="h1" aria-hidden="true" />
<Title />
</Box>
</StyledLink>
{!isDesktop ? (
@@ -3,11 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components/';
import { useCunninghamTheme } from '@/cunningham';
type TitleSemanticsProps = {
headingLevel?: 'h1' | 'h2' | 'h3';
};
export const Title = ({ headingLevel = 'h2' }: TitleSemanticsProps) => {
export const Title = () => {
const { t } = useTranslation();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
@@ -20,7 +16,7 @@ export const Title = ({ headingLevel = 'h2' }: TitleSemanticsProps) => {
>
<Text
$margin="none"
as={headingLevel}
as="h2"
$color={colorsTokens['primary-text']}
$zIndex={1}
$size="1.375rem"
@@ -48,7 +48,7 @@ export default function HomeBanner() {
$gap={spacingsTokens['sm']}
>
<IconDocs
aria-label={t('Back to homepage')}
aria-label={t('Docs Logo')}
width={64}
color={colorsTokens['primary-text']}
/>
@@ -62,7 +62,6 @@ export const HomeHeader = () => {
$height="fit-content"
>
<IconDocs
data-testid="header-icon-docs"
aria-label={t('Docs Logo')}
width={32}
color={colorsTokens['primary-text']}
@@ -39,17 +39,12 @@ export const LanguagePicker = () => {
<DropdownMenu
options={optionsPicker}
showArrow
label={t('Select language')}
buttonCss={css`
&:hover {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
}
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: 2px;
}
border-radius: 4px;
padding: 0.5rem 0.6rem;
& > div {
@@ -1,24 +1,26 @@
import { usePathname, useSearchParams } from 'next/navigation';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, StyledLink, Text } from '@/components';
import { Box, BoxButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocDefaultFilter } from '@/docs/doc-management';
import { useLeftPanelStore } from '@/features/left-panel';
export const LeftPanelTargetFilters = () => {
const { t } = useTranslation();
const pathname = usePathname();
const searchParams = useSearchParams();
const pathname = usePathname();
const { togglePanel } = useLeftPanelStore();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const searchParams = useSearchParams();
const target =
(searchParams.get('target') as DocDefaultFilter) ??
DocDefaultFilter.ALL_DOCS;
const router = useRouter();
const defaultQueries = [
{
icon: 'apps',
@@ -37,20 +39,15 @@ export const LeftPanelTargetFilters = () => {
},
];
const buildHref = (query: DocDefaultFilter) => {
const onSelectQuery = (query: DocDefaultFilter) => {
const params = new URLSearchParams(searchParams);
params.set('target', query);
return `${pathname}?${params.toString()}`;
};
const handleClick = () => {
router.push(`${pathname}?${params.toString()}`);
togglePanel();
};
return (
<Box
as="nav"
aria-label={t('Document sections')}
$justify="center"
$padding={{ horizontal: 'sm' }}
$gap={spacingsTokens['2xs']}
@@ -58,36 +55,28 @@ export const LeftPanelTargetFilters = () => {
>
{defaultQueries.map((query) => {
const isActive = target === query.targetQuery;
const href = buildHref(query.targetQuery);
return (
<StyledLink
key={query.label}
href={href}
<BoxButton
aria-label={query.label}
aria-current={isActive ? 'page' : undefined}
onClick={handleClick}
key={query.label}
onClick={() => onSelectQuery(query.targetQuery)}
$direction="row"
aria-selected={isActive}
$align="center"
$justify="flex-start"
$gap={spacingsTokens['xs']}
$radius={spacingsTokens['3xs']}
$padding={{ all: '2xs' }}
$css={css`
display: flex;
align-items: center;
justify-content: flex-start;
gap: ${spacingsTokens['xs']};
padding: ${spacingsTokens['2xs']};
border-radius: ${spacingsTokens['3xs']};
cursor: pointer;
background-color: ${isActive
? colorsTokens['greyscale-100']
: 'transparent'};
font-weight: ${isActive ? 700 : 400};
color: inherit;
text-decoration: none;
cursor: pointer;
: undefined};
font-weight: ${isActive ? 700 : undefined};
&:hover {
background-color: ${colorsTokens['greyscale-100']};
}
&:focus-visible {
outline: 2px solid ${colorsTokens['primary-500']};
outline-offset: 2px;
}
`}
>
<Icon
@@ -97,7 +86,7 @@ export const LeftPanelTargetFilters = () => {
<Text $variation={isActive ? '1000' : '700'} $size="sm">
{query.label}
</Text>
</StyledLink>
</BoxButton>
);
})}
</Box>
@@ -1,6 +1,4 @@
import { useModal } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { DateTime } from 'luxon';
import { css } from 'styled-components';
import { Box, StyledLink } from '@/components';
@@ -16,12 +14,11 @@ type LeftPanelFavoriteItemProps = {
export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
const shareModal = useModal();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const { spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
return (
<Box
as="li"
$direction="row"
$align="center"
$justify="space-between"
@@ -31,8 +28,7 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
.pinned-actions {
opacity: ${isDesktop ? 0 : 1};
}
&:hover,
&:focus-within {
&:hover {
cursor: pointer;
background-color: var(--c--theme--colors--greyscale-100);
@@ -40,20 +36,11 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
opacity: 1;
}
}
&:focus-visible {
outline: 2px solid ${colorsTokens['primary-500']};
outline-offset: 2px;
border-radius: ${spacingsTokens['3xs']};
}
`}
key={doc.id}
className="--docs--left-panel-favorite-item"
>
<StyledLink
href={`/docs/${doc.id}`}
$css="overflow: auto;"
aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`}
>
<StyledLink href={`/docs/${doc.id}`} $css="overflow: auto;">
<SimpleDocItem showAccesses doc={doc} />
</StyledLink>
<div className="pinned-actions">
@@ -23,11 +23,7 @@ export const LeftPanelFavorites = () => {
}
return (
<Box
as="nav"
aria-label={t('Pinned documents')}
className="--docs--left-panel-favorites"
>
<Box className="--docs--left-panel-favorites">
<HorizontalSeparator $withPadding={false} />
<Box
$justify="center"
@@ -45,7 +41,6 @@ export const LeftPanelFavorites = () => {
{t('Pinned documents')}
</Text>
<InfiniteScroll
as="ul"
hasMore={docs.hasNextPage}
isLoading={docs.isFetchingNextPage}
next={() => void docs.fetchNextPage()}
@@ -1,5 +1,4 @@
import { Button } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useRouter } from 'next/router';
import { PropsWithChildren, useCallback, useState } from 'react';
@@ -54,34 +53,20 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
>
<Box $direction="row" $gap="2px">
<Button
data-testid="home-button"
onClick={goToHome}
aria-label={t('Back to homepage')}
size="medium"
color="tertiary-text"
icon={
<Icon
$variation="800"
$theme="primary"
iconName="house"
aria-hidden="true"
/>
<Icon $variation="800" $theme="primary" iconName="house" />
}
/>
{authenticated && (
<Button
data-testid="search-docs-button"
onClick={openSearchModal}
size="medium"
color="tertiary-text"
aria-label={t('Search docs')}
icon={
<Icon
$variation="800"
$theme="primary"
iconName="search"
aria-hidden="true"
/>
<Icon $variation="800" $theme="primary" iconName="search" />
}
/>
)}
@@ -19,10 +19,9 @@ export const LeftPanelHeaderButton = () => {
});
return (
<Button
data-testid="new-doc-button"
color="primary"
onClick={() => createDoc()}
icon={<Icon $variation="000" iconName="add" aria-hidden="true" />}
icon={<Icon $variation="000" iconName="add" />}
disabled={isDocCreating}
>
{t('New doc')}
@@ -64,7 +64,9 @@ export class RequestSerializer {
}
public static objectToArrayBuffer(ob: Record<string, unknown>) {
return RequestSerializer.stringToArrayBuffer(JSON.stringify(ob));
return RequestSerializer.stringToArrayBuffer(
JSON.stringify(ob),
) as ArrayBuffer;
}
constructor(requestData: RequestData) {
@@ -1 +0,0 @@
export const fallbackLng = 'en';
+1 -13
View File
@@ -2,7 +2,6 @@ import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import { fallbackLng } from './config';
import resources from './translations.json';
// Add an initialization guard
@@ -17,7 +16,7 @@ if (!isInitialized && !i18next.isInitialized) {
.use(initReactI18next)
.init({
resources,
fallbackLng,
fallbackLng: 'en',
debug: false,
detection: {
order: ['cookie', 'navigator'],
@@ -36,17 +35,6 @@ if (!isInitialized && !i18next.isInitialized) {
nsSeparator: false,
keySeparator: false,
})
.then(() => {
if (typeof document !== 'undefined') {
document.documentElement.setAttribute(
'lang',
i18next.language || fallbackLng,
);
i18next.on('languageChanged', (lang) => {
document.documentElement.setAttribute('lang', lang);
});
}
})
.catch((e) => console.error('i18n initialization failed:', e));
}

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